@copilotz/chat-ui 0.1.9 → 0.1.11

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.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 useEffect10, useRef as useRef6, 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, useMemo, useEffect, memo } from "react";
242
249
  import ReactMarkdown from "react-markdown";
243
250
  import remarkGfm from "remark-gfm";
244
251
  import rehypeHighlight from "rehype-highlight";
@@ -271,6 +278,24 @@ var formatDate = (timestamp, labels) => {
271
278
  });
272
279
  }
273
280
  };
281
+ var createObjectUrlFromDataUrl = (dataUrl) => {
282
+ const match = dataUrl.match(/^data:(.+?);base64,(.+)$/s);
283
+ if (!match) {
284
+ return null;
285
+ }
286
+ try {
287
+ const [, mimeType, base64] = match;
288
+ const binary = atob(base64);
289
+ const bytes = new Uint8Array(binary.length);
290
+ for (let i = 0; i < binary.length; i += 1) {
291
+ bytes[i] = binary.charCodeAt(i);
292
+ }
293
+ const blob = new Blob([bytes], { type: mimeType || "application/octet-stream" });
294
+ return URL.createObjectURL(blob);
295
+ } catch {
296
+ return null;
297
+ }
298
+ };
274
299
 
275
300
  // src/components/ui/button.tsx
276
301
  import { jsx } from "react/jsx-runtime";
@@ -569,21 +594,89 @@ var markdownComponents = {
569
594
  var remarkPluginsDefault = [remarkGfm];
570
595
  var rehypePluginsDefault = [rehypeHighlight];
571
596
  var rehypePluginsEmpty = [];
597
+ var getPlainTextChunks = (content, chunkSize) => {
598
+ if (chunkSize <= 0 || content.length <= chunkSize) {
599
+ return [content];
600
+ }
601
+ const chunks = [];
602
+ let start = 0;
603
+ while (start < content.length) {
604
+ let end = Math.min(start + chunkSize, content.length);
605
+ if (end < content.length) {
606
+ const splitAt = content.lastIndexOf("\n", end);
607
+ if (splitAt > start + Math.floor(chunkSize / 2)) {
608
+ end = splitAt + 1;
609
+ }
610
+ }
611
+ chunks.push(content.slice(start, end));
612
+ start = end;
613
+ }
614
+ return chunks;
615
+ };
616
+ var hasCodeBlocks = (content) => /(^|\n)(```|~~~)/.test(content);
617
+ var getCollapsedPreview = (content, previewChars, previewOverride) => {
618
+ if (previewOverride && previewOverride.trim().length > 0) {
619
+ const normalizedPreview = previewOverride.trimEnd();
620
+ return normalizedPreview.endsWith("...") ? normalizedPreview : `${normalizedPreview}...`;
621
+ }
622
+ if (content.length <= previewChars) {
623
+ return content;
624
+ }
625
+ return `${content.slice(0, previewChars).trimEnd()}...`;
626
+ };
627
+ var LongContentShell = memo(function LongContentShell2({ children, className, style }) {
628
+ return /* @__PURE__ */ jsx7("div", { className, style, children });
629
+ });
630
+ var PlainTextContent = memo(function PlainTextContent2({
631
+ content,
632
+ className = "",
633
+ chunkSize = 12e3,
634
+ style
635
+ }) {
636
+ const chunks = useMemo(() => getPlainTextChunks(content, chunkSize), [content, chunkSize]);
637
+ return /* @__PURE__ */ jsx7(
638
+ LongContentShell,
639
+ {
640
+ className: `text-sm leading-6 whitespace-pre-wrap break-words ${className}`.trim(),
641
+ style,
642
+ children: chunks.map((chunk, index) => /* @__PURE__ */ jsx7(React.Fragment, { children: chunk }, index))
643
+ }
644
+ );
645
+ });
572
646
  var StreamingText = memo(function StreamingText2({
573
647
  content,
574
648
  isStreaming = false,
575
649
  thinkingLabel = "Thinking...",
576
- className = ""
650
+ className = "",
651
+ renderMarkdown = true,
652
+ plainTextChunkChars = 12e3,
653
+ contentStyle
577
654
  }) {
578
655
  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,
656
+ const enableSyntaxHighlight = renderMarkdown && !isStreaming && hasCodeBlocks(content);
657
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
658
+ hasContent ? renderMarkdown ? /* @__PURE__ */ jsx7(
659
+ LongContentShell,
582
660
  {
583
- remarkPlugins: remarkPluginsDefault,
584
- rehypePlugins: isStreaming ? rehypePluginsEmpty : rehypePluginsDefault,
585
- components: markdownComponents,
586
- children: content
661
+ className: `prose prose-sm max-w-none dark:prose-invert break-words ${className}`.trim(),
662
+ style: contentStyle,
663
+ children: /* @__PURE__ */ jsx7(
664
+ ReactMarkdown,
665
+ {
666
+ remarkPlugins: remarkPluginsDefault,
667
+ rehypePlugins: enableSyntaxHighlight ? rehypePluginsDefault : rehypePluginsEmpty,
668
+ components: markdownComponents,
669
+ children: content
670
+ }
671
+ )
672
+ }
673
+ ) : /* @__PURE__ */ jsx7(
674
+ PlainTextContent,
675
+ {
676
+ content,
677
+ className,
678
+ chunkSize: plainTextChunkChars,
679
+ style: contentStyle
587
680
  }
588
681
  ) : isStreaming ? (
589
682
  // Show thinking indicator while waiting for first token
@@ -593,26 +686,22 @@ var StreamingText = memo(function StreamingText2({
593
686
  ] });
594
687
  });
595
688
  var MediaRenderer = memo(function MediaRenderer2({ attachment }) {
596
- const [isPlaying, setIsPlaying] = useState(false);
597
- const audioRef = useRef(null);
598
- const videoRef = useRef(null);
599
- const togglePlayback = () => {
600
- if (attachment.kind === "audio" && audioRef.current) {
601
- if (isPlaying) {
602
- audioRef.current.pause();
603
- } else {
604
- audioRef.current.play();
605
- }
606
- setIsPlaying(!isPlaying);
607
- } else if (attachment.kind === "video" && videoRef.current) {
608
- if (isPlaying) {
609
- videoRef.current.pause();
610
- } else {
611
- videoRef.current.play();
612
- }
613
- setIsPlaying(!isPlaying);
689
+ const [audioPlaybackSrc, setAudioPlaybackSrc] = useState(attachment.dataUrl);
690
+ useEffect(() => {
691
+ if (attachment.kind !== "audio" || !attachment.dataUrl.startsWith("data:")) {
692
+ setAudioPlaybackSrc(attachment.dataUrl);
693
+ return;
614
694
  }
615
- };
695
+ const objectUrl = createObjectUrlFromDataUrl(attachment.dataUrl);
696
+ if (!objectUrl) {
697
+ setAudioPlaybackSrc(attachment.dataUrl);
698
+ return;
699
+ }
700
+ setAudioPlaybackSrc(objectUrl);
701
+ return () => {
702
+ URL.revokeObjectURL(objectUrl);
703
+ };
704
+ }, [attachment.kind, attachment.dataUrl]);
616
705
  const formatDuration = (ms) => {
617
706
  if (!ms) return "";
618
707
  const seconds = Math.floor(ms / 1e3);
@@ -637,13 +726,10 @@ var MediaRenderer = memo(function MediaRenderer2({ attachment }) {
637
726
  return /* @__PURE__ */ jsx7("div", { className: "flex w-full max-w-md py-0 min-w-64 items-center gap-3", children: /* @__PURE__ */ jsx7(
638
727
  "audio",
639
728
  {
640
- ref: audioRef,
641
- src: attachment.dataUrl,
642
- onPlay: () => setIsPlaying(true),
643
- onPause: () => setIsPlaying(false),
644
- onEnded: () => setIsPlaying(false),
645
729
  className: "w-full mt-2",
646
- controls: true
730
+ preload: "metadata",
731
+ controls: true,
732
+ children: /* @__PURE__ */ jsx7("source", { src: audioPlaybackSrc, type: attachment.mimeType })
647
733
  }
648
734
  ) });
649
735
  case "video":
@@ -651,14 +737,10 @@ var MediaRenderer = memo(function MediaRenderer2({ attachment }) {
651
737
  /* @__PURE__ */ jsx7(
652
738
  "video",
653
739
  {
654
- ref: videoRef,
655
740
  src: attachment.dataUrl,
656
741
  poster: attachment.poster,
657
742
  controls: true,
658
- className: "w-full h-auto",
659
- onPlay: () => setIsPlaying(true),
660
- onPause: () => setIsPlaying(false),
661
- onEnded: () => setIsPlaying(false)
743
+ className: "w-full h-auto"
662
744
  }
663
745
  ),
664
746
  attachment.fileName && /* @__PURE__ */ jsx7("div", { className: "absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-2", children: attachment.fileName })
@@ -753,6 +835,15 @@ var arePropsEqual = (prevProps, nextProps) => {
753
835
  if (prevProps.className !== nextProps.className) return false;
754
836
  if (prevProps.toolUsedLabel !== nextProps.toolUsedLabel) return false;
755
837
  if (prevProps.thinkingLabel !== nextProps.thinkingLabel) return false;
838
+ if (prevProps.showMoreLabel !== nextProps.showMoreLabel) return false;
839
+ if (prevProps.showLessLabel !== nextProps.showLessLabel) return false;
840
+ if (prevProps.collapseLongMessages !== nextProps.collapseLongMessages) return false;
841
+ if (prevProps.collapseLongMessagesForUserOnly !== nextProps.collapseLongMessagesForUserOnly) return false;
842
+ if (prevProps.longMessagePreviewChars !== nextProps.longMessagePreviewChars) return false;
843
+ if (prevProps.longMessageChunkChars !== nextProps.longMessageChunkChars) return false;
844
+ if (prevProps.renderUserMarkdown !== nextProps.renderUserMarkdown) return false;
845
+ if (prevProps.isExpanded !== nextProps.isExpanded) return false;
846
+ if (prevProps.onToggleExpanded !== nextProps.onToggleExpanded) return false;
756
847
  if (prevProps.isGrouped !== nextProps.isGrouped) return false;
757
848
  if (prevProps.assistantAvatar !== nextProps.assistantAvatar) return false;
758
849
  return true;
@@ -775,6 +866,15 @@ var Message = memo(({
775
866
  className = "",
776
867
  toolUsedLabel,
777
868
  thinkingLabel = "Thinking...",
869
+ showMoreLabel = "Show more",
870
+ showLessLabel = "Show less",
871
+ collapseLongMessages = false,
872
+ collapseLongMessagesForUserOnly = false,
873
+ longMessagePreviewChars = 4e3,
874
+ longMessageChunkChars = 12e3,
875
+ renderUserMarkdown = true,
876
+ isExpanded = false,
877
+ onToggleExpanded,
778
878
  isGrouped = false
779
879
  }) => {
780
880
  const [isEditing, setIsEditing] = useState(false);
@@ -784,6 +884,18 @@ var Message = memo(({
784
884
  const messageIsUser = isUser ?? message.role === "user";
785
885
  const canEdit = enableEdit && messageIsUser;
786
886
  const canRegenerate = enableRegenerate && !messageIsUser;
887
+ const normalizedPreviewChars = Math.max(longMessagePreviewChars, 1);
888
+ const normalizedChunkChars = Math.max(longMessageChunkChars, 1);
889
+ const previewOverride = typeof message.metadata?.previewContent === "string" ? message.metadata.previewContent : void 0;
890
+ const canCollapseMessage = collapseLongMessages && !message.isStreaming && message.content.length > normalizedPreviewChars && (!collapseLongMessagesForUserOnly || messageIsUser);
891
+ const isCollapsed = canCollapseMessage && !isExpanded;
892
+ const contentToRender = isCollapsed ? getCollapsedPreview(message.content, normalizedPreviewChars, previewOverride) : message.content;
893
+ const shouldRenderMarkdown = !isCollapsed && (!messageIsUser || renderUserMarkdown);
894
+ const shouldApplyLargeContentContainment = !isCollapsed && message.content.length > normalizedChunkChars;
895
+ const contentStyle = shouldApplyLargeContentContainment ? {
896
+ contentVisibility: "auto",
897
+ containIntrinsicSize: "1px 400px"
898
+ } : void 0;
787
899
  const handleCopy = async () => {
788
900
  try {
789
901
  await navigator.clipboard.writeText(message.content);
@@ -812,6 +924,9 @@ var Message = memo(({
812
924
  const handleRegenerate = () => {
813
925
  onAction?.({ action: "regenerate", messageId: message.id });
814
926
  };
927
+ const handleToggleExpanded = () => {
928
+ onToggleExpanded?.(message.id);
929
+ };
815
930
  const formatTime = (timestamp) => {
816
931
  return new Date(timestamp).toLocaleTimeString("pt-BR", {
817
932
  hour: "2-digit",
@@ -862,11 +977,26 @@ var Message = memo(({
862
977
  /* @__PURE__ */ jsx7(
863
978
  StreamingText,
864
979
  {
865
- content: message.content,
980
+ content: contentToRender,
866
981
  isStreaming: message.isStreaming,
867
- thinkingLabel
982
+ thinkingLabel,
983
+ renderMarkdown: shouldRenderMarkdown,
984
+ plainTextChunkChars: normalizedChunkChars,
985
+ contentStyle
868
986
  }
869
987
  ),
988
+ canCollapseMessage && /* @__PURE__ */ jsx7("div", { className: "mt-3", children: /* @__PURE__ */ jsx7(
989
+ Button,
990
+ {
991
+ type: "button",
992
+ variant: "ghost",
993
+ size: "sm",
994
+ className: "h-auto px-0 text-xs font-medium text-current hover:bg-transparent hover:opacity-80",
995
+ "aria-expanded": !isCollapsed,
996
+ onClick: handleToggleExpanded,
997
+ children: isCollapsed ? showMoreLabel : showLessLabel
998
+ }
999
+ ) }),
870
1000
  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
1001
  ] }),
872
1002
  !isEditing && (showActions || copied) && /* @__PURE__ */ jsxs2("div", { className: `absolute -top-2 flex gap-1 ${messageIsUser ? "-left-2" : "-right-2"}`, children: [
@@ -917,7 +1047,7 @@ var Message = memo(({
917
1047
  }, arePropsEqual);
918
1048
 
919
1049
  // src/components/chat/Sidebar.tsx
920
- import { useState as useState4, useRef as useRef5, useEffect as useEffect6 } from "react";
1050
+ import { useState as useState4, useRef as useRef4, useEffect as useEffect7 } from "react";
921
1051
 
922
1052
  // src/components/ui/input.tsx
923
1053
  import { jsx as jsx8 } from "react/jsx-runtime";
@@ -2166,9 +2296,9 @@ var Sidebar2 = ({
2166
2296
  const [deleteThreadId, setDeleteThreadId] = useState4(null);
2167
2297
  const [editingThreadId, setEditingThreadId] = useState4(null);
2168
2298
  const [editTitle, setEditTitle] = useState4("");
2169
- const inputRef = useRef5(null);
2299
+ const inputRef = useRef4(null);
2170
2300
  const { setOpen } = useSidebar();
2171
- useEffect6(() => {
2301
+ useEffect7(() => {
2172
2302
  if (editingThreadId && inputRef.current) {
2173
2303
  inputRef.current.focus();
2174
2304
  inputRef.current.select();
@@ -2585,10 +2715,10 @@ var ChatHeader = ({
2585
2715
  };
2586
2716
 
2587
2717
  // src/components/chat/ChatInput.tsx
2588
- import { useState as useState6, useRef as useRef6, useCallback as useCallback3, useEffect as useEffect8, memo as memo2 } from "react";
2718
+ import { useState as useState6, useRef as useRef5, useCallback as useCallback3, useEffect as useEffect9, memo as memo2 } from "react";
2589
2719
 
2590
2720
  // 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";
2721
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect8, useMemo as useMemo3, useState as useState5 } from "react";
2592
2722
  import { jsx as jsx19 } from "react/jsx-runtime";
2593
2723
  var Ctx = createContext2(void 0);
2594
2724
  var ChatUserContextProvider = ({ children, initial }) => {
@@ -2596,7 +2726,7 @@ var ChatUserContextProvider = ({ children, initial }) => {
2596
2726
  updatedAt: Date.now(),
2597
2727
  ...initial ?? {}
2598
2728
  }));
2599
- useEffect7(() => {
2729
+ useEffect8(() => {
2600
2730
  if (!initial) return;
2601
2731
  setCtx((prev) => ({
2602
2732
  ...prev,
@@ -2610,7 +2740,7 @@ var ChatUserContextProvider = ({ children, initial }) => {
2610
2740
  return { ...prev, ...partial, updatedAt: Date.now() };
2611
2741
  });
2612
2742
  }, []);
2613
- const value = useMemo2(() => ({
2743
+ const value = useMemo3(() => ({
2614
2744
  context: ctx,
2615
2745
  setContext: setPartial,
2616
2746
  resetContext: () => setCtx({ updatedAt: Date.now() })
@@ -2662,8 +2792,8 @@ import {
2662
2792
  FileText,
2663
2793
  X as X2,
2664
2794
  Square,
2665
- Play as Play2,
2666
- Pause as Pause2,
2795
+ Play,
2796
+ Pause,
2667
2797
  Loader2
2668
2798
  } from "lucide-react";
2669
2799
  import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs11 } from "react/jsx-runtime";
@@ -2728,7 +2858,23 @@ var FileUploadItem = memo2(function FileUploadItem2({ file, progress, onCancel }
2728
2858
  });
2729
2859
  var AttachmentPreview = memo2(function AttachmentPreview2({ attachment, onRemove }) {
2730
2860
  const [isPlaying, setIsPlaying] = useState6(false);
2731
- const audioRef = useRef6(null);
2861
+ const [audioPlaybackSrc, setAudioPlaybackSrc] = useState6(attachment.dataUrl);
2862
+ const audioRef = useRef5(null);
2863
+ useEffect9(() => {
2864
+ if (attachment.kind !== "audio" || !attachment.dataUrl.startsWith("data:")) {
2865
+ setAudioPlaybackSrc(attachment.dataUrl);
2866
+ return;
2867
+ }
2868
+ const objectUrl = createObjectUrlFromDataUrl(attachment.dataUrl);
2869
+ if (!objectUrl) {
2870
+ setAudioPlaybackSrc(attachment.dataUrl);
2871
+ return;
2872
+ }
2873
+ setAudioPlaybackSrc(objectUrl);
2874
+ return () => {
2875
+ URL.revokeObjectURL(objectUrl);
2876
+ };
2877
+ }, [attachment.kind, attachment.dataUrl]);
2732
2878
  const handlePlayPause = () => {
2733
2879
  if (audioRef.current) {
2734
2880
  if (isPlaying) {
@@ -2796,7 +2942,7 @@ var AttachmentPreview = memo2(function AttachmentPreview2({ attachment, onRemove
2796
2942
  size: "icon",
2797
2943
  className: "h-8 w-8",
2798
2944
  onClick: handlePlayPause,
2799
- children: isPlaying ? /* @__PURE__ */ jsx21(Pause2, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx21(Play2, { className: "h-3 w-3" })
2945
+ children: isPlaying ? /* @__PURE__ */ jsx21(Pause, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx21(Play, { className: "h-3 w-3" })
2800
2946
  }
2801
2947
  ),
2802
2948
  /* @__PURE__ */ jsxs11("div", { className: "flex-1", children: [
@@ -2807,10 +2953,11 @@ var AttachmentPreview = memo2(function AttachmentPreview2({ attachment, onRemove
2807
2953
  "audio",
2808
2954
  {
2809
2955
  ref: audioRef,
2810
- src: attachment.dataUrl,
2811
2956
  onPlay: () => setIsPlaying(true),
2812
2957
  onPause: () => setIsPlaying(false),
2813
- onEnded: () => setIsPlaying(false)
2958
+ onEnded: () => setIsPlaying(false),
2959
+ preload: "metadata",
2960
+ children: /* @__PURE__ */ jsx21("source", { src: audioPlaybackSrc, type: attachment.mimeType })
2814
2961
  }
2815
2962
  ),
2816
2963
  /* @__PURE__ */ jsx21(
@@ -2905,13 +3052,13 @@ var ChatInput = memo2(function ChatInput2({
2905
3052
  const { setContext } = useChatUserContext();
2906
3053
  const [recordingDuration, setRecordingDuration] = useState6(0);
2907
3054
  const [uploadProgress, setUploadProgress] = useState6(/* @__PURE__ */ new Map());
2908
- const textareaRef = useRef6(null);
2909
- const fileInputRef = useRef6(null);
2910
- const mediaRecorderRef = useRef6(null);
2911
- const recordingStartTime = useRef6(0);
2912
- const recordingInterval = useRef6(null);
2913
- const mediaStreamRef = useRef6(null);
2914
- useEffect8(() => {
3055
+ const textareaRef = useRef5(null);
3056
+ const fileInputRef = useRef5(null);
3057
+ const mediaRecorderRef = useRef5(null);
3058
+ const recordingStartTime = useRef5(0);
3059
+ const recordingInterval = useRef5(null);
3060
+ const mediaStreamRef = useRef5(null);
3061
+ useEffect9(() => {
2915
3062
  return () => {
2916
3063
  if (mediaStreamRef.current) {
2917
3064
  mediaStreamRef.current.getTracks().forEach((track) => track.stop());
@@ -3735,7 +3882,7 @@ var ChatUI = ({
3735
3882
  initialInput,
3736
3883
  onInitialInputConsumed
3737
3884
  }) => {
3738
- const config = useMemo3(
3885
+ const config = useMemo4(
3739
3886
  () => mergeConfig(defaultChatConfig, userConfig),
3740
3887
  [userConfig]
3741
3888
  );
@@ -3756,6 +3903,7 @@ var ChatUI = ({
3756
3903
  };
3757
3904
  const [inputValue, setInputValue] = useState8("");
3758
3905
  const [attachments, setAttachments] = useState8([]);
3906
+ const [expandedMessageIds, setExpandedMessageIds] = useState8({});
3759
3907
  const [state, setState] = useState8({
3760
3908
  isRecording: false,
3761
3909
  selectedThreadId: currentThreadId,
@@ -3768,30 +3916,30 @@ var ChatUI = ({
3768
3916
  isSidebarCollapsed: false
3769
3917
  // No longer used for main sidebar
3770
3918
  });
3771
- useEffect9(() => {
3919
+ useEffect10(() => {
3772
3920
  if (currentThreadId !== state.selectedThreadId) {
3773
3921
  setState((prev) => ({ ...prev, selectedThreadId: currentThreadId }));
3774
3922
  }
3775
3923
  }, [currentThreadId]);
3776
- const initialInputApplied = useRef7(false);
3777
- const initialInputConsumedRef = useRef7(false);
3778
- useEffect9(() => {
3924
+ const initialInputApplied = useRef6(false);
3925
+ const initialInputConsumedRef = useRef6(false);
3926
+ useEffect10(() => {
3779
3927
  if (initialInput && !initialInputApplied.current) {
3780
3928
  setInputValue(initialInput);
3781
3929
  initialInputApplied.current = true;
3782
3930
  }
3783
3931
  }, [initialInput]);
3784
- const scrollAreaRef = useRef7(null);
3785
- const stateRef = useRef7(state);
3786
- const inputValueRef = useRef7(inputValue);
3787
- const attachmentsRef = useRef7(attachments);
3788
- useEffect9(() => {
3932
+ const scrollAreaRef = useRef6(null);
3933
+ const stateRef = useRef6(state);
3934
+ const inputValueRef = useRef6(inputValue);
3935
+ const attachmentsRef = useRef6(attachments);
3936
+ useEffect10(() => {
3789
3937
  stateRef.current = state;
3790
3938
  }, [state]);
3791
- useEffect9(() => {
3939
+ useEffect10(() => {
3792
3940
  inputValueRef.current = inputValue;
3793
3941
  }, [inputValue]);
3794
- useEffect9(() => {
3942
+ useEffect10(() => {
3795
3943
  attachmentsRef.current = attachments;
3796
3944
  }, [attachments]);
3797
3945
  const [isCustomMounted, setIsCustomMounted] = useState8(false);
@@ -3814,7 +3962,7 @@ var ChatUI = ({
3814
3962
  []
3815
3963
  // No dependencies - uses refs for latest state
3816
3964
  );
3817
- useEffect9(() => {
3965
+ useEffect10(() => {
3818
3966
  const checkMobile = () => {
3819
3967
  setIsMobile(globalThis.innerWidth < 1024);
3820
3968
  };
@@ -3822,7 +3970,7 @@ var ChatUI = ({
3822
3970
  globalThis.addEventListener("resize", checkMobile);
3823
3971
  return () => globalThis.removeEventListener("resize", checkMobile);
3824
3972
  }, []);
3825
- useEffect9(() => {
3973
+ useEffect10(() => {
3826
3974
  if (!isMobile || !config.customComponent?.component) return;
3827
3975
  if (state.showSidebar) {
3828
3976
  setIsCustomMounted(true);
@@ -3833,8 +3981,8 @@ var ChatUI = ({
3833
3981
  return () => clearTimeout(t);
3834
3982
  }
3835
3983
  }, [state.showSidebar, isMobile, config.customComponent]);
3836
- const prevMessageCountRef = useRef7(0);
3837
- useEffect9(() => {
3984
+ const prevMessageCountRef = useRef6(0);
3985
+ useEffect10(() => {
3838
3986
  if (messages.length === 0) {
3839
3987
  prevMessageCountRef.current = 0;
3840
3988
  return;
@@ -3860,6 +4008,24 @@ var ChatUI = ({
3860
4008
  }
3861
4009
  });
3862
4010
  }, [messages, state.isAtBottom, virtualizer]);
4011
+ useEffect10(() => {
4012
+ virtualizer.measure();
4013
+ }, [expandedMessageIds, virtualizer]);
4014
+ useEffect10(() => {
4015
+ const validMessageIds = new Set(messages.map((message) => message.id));
4016
+ setExpandedMessageIds((prev) => {
4017
+ const activeIds = Object.keys(prev);
4018
+ const staleIds = activeIds.filter((messageId) => !validMessageIds.has(messageId));
4019
+ if (staleIds.length === 0) {
4020
+ return prev;
4021
+ }
4022
+ const next = { ...prev };
4023
+ staleIds.forEach((messageId) => {
4024
+ delete next[messageId];
4025
+ });
4026
+ return next;
4027
+ });
4028
+ }, [messages]);
3863
4029
  const handleScroll = useCallback4((e) => {
3864
4030
  const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
3865
4031
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
@@ -3897,6 +4063,19 @@ var ChatUI = ({
3897
4063
  break;
3898
4064
  }
3899
4065
  }, [callbacks, createStateCallback]);
4066
+ const handleToggleMessageExpansion = useCallback4((messageId) => {
4067
+ setExpandedMessageIds((prev) => {
4068
+ if (prev[messageId]) {
4069
+ const next = { ...prev };
4070
+ delete next[messageId];
4071
+ return next;
4072
+ }
4073
+ return {
4074
+ ...prev,
4075
+ [messageId]: true
4076
+ };
4077
+ });
4078
+ }, []);
3900
4079
  const handleCreateThread = useCallback4((title) => {
3901
4080
  callbacks.onCreateThread?.(title, createStateCallback());
3902
4081
  }, [callbacks, createStateCallback]);
@@ -3987,7 +4166,7 @@ var ChatUI = ({
3987
4166
  `message-skeleton-${index}`
3988
4167
  );
3989
4168
  }) });
3990
- const messageProps = useMemo3(() => ({
4169
+ const messageProps = useMemo4(() => ({
3991
4170
  userAvatar: user?.avatar,
3992
4171
  userName: user?.name,
3993
4172
  assistantAvatar: assistant?.avatar,
@@ -4001,7 +4180,15 @@ var ChatUI = ({
4001
4180
  compactMode: config.ui.compactMode,
4002
4181
  onAction: handleMessageAction,
4003
4182
  toolUsedLabel: config.labels.toolUsed,
4004
- thinkingLabel: config.labels.thinking
4183
+ thinkingLabel: config.labels.thinking,
4184
+ showMoreLabel: config.labels.showMoreMessage,
4185
+ showLessLabel: config.labels.showLessMessage,
4186
+ collapseLongMessages: config.ui.collapseLongMessages,
4187
+ collapseLongMessagesForUserOnly: config.ui.collapseLongMessagesForUserOnly,
4188
+ longMessagePreviewChars: config.ui.longMessagePreviewChars,
4189
+ longMessageChunkChars: config.ui.longMessageChunkChars,
4190
+ renderUserMarkdown: config.ui.renderUserMarkdown,
4191
+ onToggleExpanded: handleToggleMessageExpansion
4005
4192
  }), [
4006
4193
  user?.avatar,
4007
4194
  user?.name,
@@ -4016,7 +4203,15 @@ var ChatUI = ({
4016
4203
  config.features.enableToolCallsDisplay,
4017
4204
  config.labels.toolUsed,
4018
4205
  config.labels.thinking,
4019
- handleMessageAction
4206
+ config.labels.showMoreMessage,
4207
+ config.labels.showLessMessage,
4208
+ config.ui.collapseLongMessages,
4209
+ config.ui.collapseLongMessagesForUserOnly,
4210
+ config.ui.longMessagePreviewChars,
4211
+ config.ui.longMessageChunkChars,
4212
+ config.ui.renderUserMarkdown,
4213
+ handleMessageAction,
4214
+ handleToggleMessageExpansion
4020
4215
  ]);
4021
4216
  const shouldShowAgentSelector = Boolean(
4022
4217
  config.agentSelector?.enabled && onSelectAgent && agentOptions.length > 0 && (!config.agentSelector?.hideIfSingle || agentOptions.length > 1)
@@ -4108,7 +4303,8 @@ var ChatUI = ({
4108
4303
  {
4109
4304
  message,
4110
4305
  ...messageProps,
4111
- isGrouped
4306
+ isGrouped,
4307
+ isExpanded: Boolean(expandedMessageIds[message.id])
4112
4308
  }
4113
4309
  ),
4114
4310
  message.role === "assistant" && renderInlineSuggestions(message.id)
@@ -4197,7 +4393,7 @@ var ChatUI = ({
4197
4393
  };
4198
4394
 
4199
4395
  // src/components/chat/ThreadManager.tsx
4200
- import { useState as useState9, useRef as useRef8, useEffect as useEffect10 } from "react";
4396
+ import { useState as useState9, useRef as useRef7, useEffect as useEffect11 } from "react";
4201
4397
  import {
4202
4398
  Plus as Plus4,
4203
4399
  MessageSquare as MessageSquare2,
@@ -4216,8 +4412,8 @@ import { Fragment as Fragment6, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-r
4216
4412
  var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onArchive }) => {
4217
4413
  const [isEditing, setIsEditing] = useState9(false);
4218
4414
  const [editTitle, setEditTitle] = useState9(thread.title);
4219
- const inputRef = useRef8(null);
4220
- useEffect10(() => {
4415
+ const inputRef = useRef7(null);
4416
+ useEffect11(() => {
4221
4417
  if (isEditing && inputRef.current) {
4222
4418
  inputRef.current.focus();
4223
4419
  inputRef.current.select();
@@ -4511,6 +4707,7 @@ export {
4511
4707
  chatUtils,
4512
4708
  cn,
4513
4709
  configUtils,
4710
+ createObjectUrlFromDataUrl,
4514
4711
  defaultChatConfig,
4515
4712
  featureFlags,
4516
4713
  formatDate,