@copilotz/chat-ui 0.1.6 → 0.1.8

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,6 @@
1
1
  // src/components/chat/ChatUI.tsx
2
- import { useState as useState8, useEffect as useEffect10, 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 useMemo3 } from "react";
3
+ import { useVirtualizer } from "@tanstack/react-virtual";
3
4
 
4
5
  // src/config/chatConfig.ts
5
6
  var defaultChatConfig = {
@@ -571,10 +572,11 @@ var rehypePluginsEmpty = [];
571
572
  var StreamingText = memo(function StreamingText2({
572
573
  content,
573
574
  isStreaming = false,
574
- thinkingLabel = "Thinking..."
575
+ thinkingLabel = "Thinking...",
576
+ className = ""
575
577
  }) {
576
578
  const hasContent = content.trim().length > 0;
577
- return /* @__PURE__ */ jsxs2("div", { className: "prose prose-sm max-w-none dark:prose-invert", children: [
579
+ return /* @__PURE__ */ jsxs2("div", { className: `prose prose-sm max-w-none dark:prose-invert break-words ${className}`, children: [
578
580
  hasContent ? /* @__PURE__ */ jsx7(
579
581
  ReactMarkdown,
580
582
  {
@@ -816,7 +818,7 @@ var Message = memo(({
816
818
  minute: "2-digit"
817
819
  });
818
820
  };
819
- return /* @__PURE__ */ jsx7(TooltipProvider, { children: /* @__PURE__ */ jsxs2(
821
+ return /* @__PURE__ */ jsxs2(
820
822
  "div",
821
823
  {
822
824
  className: `flex w-full flex-col ${className} max-w-[800px] mx-auto`,
@@ -834,7 +836,7 @@ var Message = memo(({
834
836
  message.isEdited && /* @__PURE__ */ jsx7(Badge, { variant: "outline", className: "text-xs", children: "editado" })
835
837
  ] })
836
838
  ] }),
837
- /* @__PURE__ */ jsx7("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__ */ jsxs2("div", { className: `relative inline-flex flex-col ${messageIsUser ? "rounded-lg p-3 bg-primary text-primary-foreground ml-auto max-w-[85%]" : "max-w-[85%]"}`, children: [
839
+ /* @__PURE__ */ jsx7("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__ */ jsxs2("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: [
838
840
  isEditing ? /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
839
841
  /* @__PURE__ */ jsx7(
840
842
  Textarea,
@@ -862,7 +864,8 @@ var Message = memo(({
862
864
  {
863
865
  content: message.content,
864
866
  isStreaming: message.isStreaming,
865
- thinkingLabel
867
+ thinkingLabel,
868
+ className: messageIsUser ? "[&_*]:text-right" : ""
866
869
  }
867
870
  ),
868
871
  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)) })
@@ -911,11 +914,11 @@ var Message = memo(({
911
914
  ] }) })
912
915
  ]
913
916
  }
914
- ) });
917
+ );
915
918
  }, arePropsEqual);
916
919
 
917
920
  // src/components/chat/Sidebar.tsx
918
- import { useState as useState4, useRef as useRef5, useEffect as useEffect7 } from "react";
921
+ import { useState as useState4, useRef as useRef5, useEffect as useEffect6 } from "react";
919
922
 
920
923
  // src/components/ui/input.tsx
921
924
  import { jsx as jsx8 } from "react/jsx-runtime";
@@ -2166,7 +2169,7 @@ var Sidebar2 = ({
2166
2169
  const [editTitle, setEditTitle] = useState4("");
2167
2170
  const inputRef = useRef5(null);
2168
2171
  const { setOpen } = useSidebar();
2169
- useEffect7(() => {
2172
+ useEffect6(() => {
2170
2173
  if (editingThreadId && inputRef.current) {
2171
2174
  inputRef.current.focus();
2172
2175
  inputRef.current.select();
@@ -2583,10 +2586,10 @@ var ChatHeader = ({
2583
2586
  };
2584
2587
 
2585
2588
  // src/components/chat/ChatInput.tsx
2586
- import { useState as useState6, useRef as useRef6, useCallback as useCallback3, useEffect as useEffect9, memo as memo2 } from "react";
2589
+ import { useState as useState6, useRef as useRef6, useCallback as useCallback3, useEffect as useEffect8, memo as memo2 } from "react";
2587
2590
 
2588
2591
  // src/components/chat/UserContext.tsx
2589
- import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect8, useMemo as useMemo2, useState as useState5 } from "react";
2592
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useState as useState5 } from "react";
2590
2593
  import { jsx as jsx19 } from "react/jsx-runtime";
2591
2594
  var Ctx = createContext2(void 0);
2592
2595
  var ChatUserContextProvider = ({ children, initial }) => {
@@ -2594,7 +2597,7 @@ var ChatUserContextProvider = ({ children, initial }) => {
2594
2597
  updatedAt: Date.now(),
2595
2598
  ...initial ?? {}
2596
2599
  }));
2597
- useEffect8(() => {
2600
+ useEffect7(() => {
2598
2601
  if (!initial) return;
2599
2602
  setCtx((prev) => ({
2600
2603
  ...prev,
@@ -2909,7 +2912,7 @@ var ChatInput = memo2(function ChatInput2({
2909
2912
  const recordingStartTime = useRef6(0);
2910
2913
  const recordingInterval = useRef6(null);
2911
2914
  const mediaStreamRef = useRef6(null);
2912
- useEffect9(() => {
2915
+ useEffect8(() => {
2913
2916
  return () => {
2914
2917
  if (mediaStreamRef.current) {
2915
2918
  mediaStreamRef.current.getTracks().forEach((track) => track.stop());
@@ -3180,7 +3183,7 @@ var ChatInput = memo2(function ChatInput2({
3180
3183
  onChange: (e) => onChange(e.target.value),
3181
3184
  onKeyDown: handleKeyDown,
3182
3185
  placeholder,
3183
- disabled: disabled || isGenerating,
3186
+ disabled,
3184
3187
  className: "max-h-[120px] resize-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0",
3185
3188
  rows: 1
3186
3189
  }
@@ -3709,7 +3712,7 @@ var UserProfile = ({
3709
3712
 
3710
3713
  // src/components/chat/ChatUI.tsx
3711
3714
  import { Sparkles, ArrowRight, MessageSquare, Lightbulb as Lightbulb2, Zap, HelpCircle } from "lucide-react";
3712
- import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs14 } from "react/jsx-runtime";
3715
+ import { jsx as jsx24, jsxs as jsxs14 } from "react/jsx-runtime";
3713
3716
  var ChatUI = ({
3714
3717
  messages = [],
3715
3718
  threads = [],
@@ -3766,35 +3769,40 @@ var ChatUI = ({
3766
3769
  isSidebarCollapsed: false
3767
3770
  // No longer used for main sidebar
3768
3771
  });
3769
- useEffect10(() => {
3772
+ useEffect9(() => {
3770
3773
  if (currentThreadId !== state.selectedThreadId) {
3771
3774
  setState((prev) => ({ ...prev, selectedThreadId: currentThreadId }));
3772
3775
  }
3773
3776
  }, [currentThreadId]);
3774
3777
  const initialInputApplied = useRef7(false);
3775
3778
  const initialInputConsumedRef = useRef7(false);
3776
- useEffect10(() => {
3779
+ useEffect9(() => {
3777
3780
  if (initialInput && !initialInputApplied.current) {
3778
3781
  setInputValue(initialInput);
3779
3782
  initialInputApplied.current = true;
3780
3783
  }
3781
3784
  }, [initialInput]);
3782
- const messagesEndRef = useRef7(null);
3783
3785
  const scrollAreaRef = useRef7(null);
3784
3786
  const stateRef = useRef7(state);
3785
3787
  const inputValueRef = useRef7(inputValue);
3786
3788
  const attachmentsRef = useRef7(attachments);
3787
- useEffect10(() => {
3789
+ useEffect9(() => {
3788
3790
  stateRef.current = state;
3789
3791
  }, [state]);
3790
- useEffect10(() => {
3792
+ useEffect9(() => {
3791
3793
  inputValueRef.current = inputValue;
3792
3794
  }, [inputValue]);
3793
- useEffect10(() => {
3795
+ useEffect9(() => {
3794
3796
  attachmentsRef.current = attachments;
3795
3797
  }, [attachments]);
3796
3798
  const [isCustomMounted, setIsCustomMounted] = useState8(false);
3797
3799
  const [isCustomVisible, setIsCustomVisible] = useState8(false);
3800
+ const virtualizer = useVirtualizer({
3801
+ count: messages.length,
3802
+ getScrollElement: () => scrollAreaRef.current,
3803
+ estimateSize: () => 100,
3804
+ overscan: 5
3805
+ });
3798
3806
  const createStateCallback = useCallback4(
3799
3807
  (setter) => ({
3800
3808
  setState: (newState) => setter?.(newState),
@@ -3807,7 +3815,7 @@ var ChatUI = ({
3807
3815
  []
3808
3816
  // No dependencies - uses refs for latest state
3809
3817
  );
3810
- useEffect10(() => {
3818
+ useEffect9(() => {
3811
3819
  const checkMobile = () => {
3812
3820
  setIsMobile(globalThis.innerWidth < 1024);
3813
3821
  };
@@ -3815,7 +3823,7 @@ var ChatUI = ({
3815
3823
  globalThis.addEventListener("resize", checkMobile);
3816
3824
  return () => globalThis.removeEventListener("resize", checkMobile);
3817
3825
  }, []);
3818
- useEffect10(() => {
3826
+ useEffect9(() => {
3819
3827
  if (!isMobile || !config.customComponent?.component) return;
3820
3828
  if (state.showSidebar) {
3821
3829
  setIsCustomMounted(true);
@@ -3826,21 +3834,25 @@ var ChatUI = ({
3826
3834
  return () => clearTimeout(t);
3827
3835
  }
3828
3836
  }, [state.showSidebar, isMobile, config.customComponent]);
3829
- useEffect10(() => {
3830
- if (!state.isAtBottom) return;
3831
- const viewport = scrollAreaRef.current;
3832
- if (!viewport) return;
3833
- const target = viewport.scrollHeight;
3834
- try {
3835
- viewport.scrollTo({ top: target, behavior: "smooth" });
3836
- } catch {
3837
- viewport.scrollTop = target;
3838
- }
3837
+ useEffect9(() => {
3838
+ if (!state.isAtBottom || messages.length === 0) return;
3839
+ requestAnimationFrame(() => {
3840
+ const viewport = scrollAreaRef.current;
3841
+ if (!viewport) return;
3842
+ try {
3843
+ viewport.scrollTo({ top: viewport.scrollHeight, behavior: "smooth" });
3844
+ } catch {
3845
+ viewport.scrollTop = viewport.scrollHeight;
3846
+ }
3847
+ });
3839
3848
  }, [messages, state.isAtBottom]);
3840
3849
  const handleScroll = useCallback4((e) => {
3841
3850
  const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
3842
3851
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
3843
- setState((prev) => ({ ...prev, isAtBottom }));
3852
+ setState((prev) => {
3853
+ if (prev.isAtBottom === isAtBottom) return prev;
3854
+ return { ...prev, isAtBottom };
3855
+ });
3844
3856
  }, []);
3845
3857
  const handleSendMessage = useCallback4((content, messageAttachments = []) => {
3846
3858
  if (!content.trim() && messageAttachments.length === 0) return;
@@ -3961,49 +3973,26 @@ var ChatUI = ({
3961
3973
  `message-skeleton-${index}`
3962
3974
  );
3963
3975
  }) });
3964
- const renderedMessageList = useMemo3(() => {
3965
- if (isMessagesLoading) return renderMessageLoadingSkeleton();
3966
- return /* @__PURE__ */ jsxs14(Fragment6, { children: [
3967
- renderSuggestions(),
3968
- messages.map((message, index) => {
3969
- const prevMessage = index > 0 ? messages[index - 1] : null;
3970
- const isGrouped = prevMessage !== null && prevMessage.role === message.role;
3971
- return /* @__PURE__ */ jsxs14("div", { className: isGrouped ? "space-y-1 -mt-2" : "space-y-2", children: [
3972
- /* @__PURE__ */ jsx24(
3973
- Message,
3974
- {
3975
- message,
3976
- userAvatar: user?.avatar,
3977
- userName: user?.name,
3978
- assistantAvatar: assistant?.avatar,
3979
- assistantName: assistant?.name,
3980
- showTimestamp: config.ui.showTimestamps,
3981
- showAvatar: config.ui.showAvatars,
3982
- enableCopy: config.features.enableMessageCopy,
3983
- enableEdit: config.features.enableMessageEditing,
3984
- enableRegenerate: config.features.enableRegeneration,
3985
- enableToolCallsDisplay: config.features.enableToolCallsDisplay,
3986
- compactMode: config.ui.compactMode,
3987
- onAction: handleMessageAction,
3988
- toolUsedLabel: config.labels.toolUsed,
3989
- thinkingLabel: config.labels.thinking,
3990
- isGrouped
3991
- }
3992
- ),
3993
- message.role === "assistant" && renderInlineSuggestions(message.id)
3994
- ] }, message.id);
3995
- })
3996
- ] });
3997
- }, [
3998
- isMessagesLoading,
3999
- messages,
4000
- handleSendMessage,
3976
+ const messageProps = useMemo3(() => ({
3977
+ userAvatar: user?.avatar,
3978
+ userName: user?.name,
3979
+ assistantAvatar: assistant?.avatar,
3980
+ assistantName: assistant?.name,
3981
+ showTimestamp: config.ui.showTimestamps,
3982
+ showAvatar: config.ui.showAvatars,
3983
+ enableCopy: config.features.enableMessageCopy,
3984
+ enableEdit: config.features.enableMessageEditing,
3985
+ enableRegenerate: config.features.enableRegeneration,
3986
+ enableToolCallsDisplay: config.features.enableToolCallsDisplay,
3987
+ compactMode: config.ui.compactMode,
3988
+ onAction: handleMessageAction,
3989
+ toolUsedLabel: config.labels.toolUsed,
3990
+ thinkingLabel: config.labels.thinking
3991
+ }), [
4001
3992
  user?.avatar,
4002
3993
  user?.name,
4003
3994
  assistant?.avatar,
4004
3995
  assistant?.name,
4005
- config.branding.title,
4006
- config.branding.subtitle,
4007
3996
  config.ui.showTimestamps,
4008
3997
  config.ui.showAvatars,
4009
3998
  config.ui.compactMode,
@@ -4013,9 +4002,7 @@ var ChatUI = ({
4013
4002
  config.features.enableToolCallsDisplay,
4014
4003
  config.labels.toolUsed,
4015
4004
  config.labels.thinking,
4016
- handleMessageAction,
4017
- messageSuggestions,
4018
- suggestions
4005
+ handleMessageAction
4019
4006
  ]);
4020
4007
  const shouldShowAgentSelector = Boolean(
4021
4008
  config.agentSelector?.enabled && onSelectAgent && agentOptions.length > 0 && (!config.agentSelector?.hideIfSingle || agentOptions.length > 1)
@@ -4076,10 +4063,48 @@ var ChatUI = ({
4076
4063
  className: "flex-1 min-h-0",
4077
4064
  viewportClassName: "p-4 overscroll-contain",
4078
4065
  onScrollCapture: handleScroll,
4079
- children: /* @__PURE__ */ jsxs14("div", { className: "max-w-4xl mx-auto space-y-4 pb-4", children: [
4080
- renderedMessageList,
4081
- /* @__PURE__ */ jsx24("div", { ref: messagesEndRef })
4082
- ] })
4066
+ style: { contain: "strict" },
4067
+ children: /* @__PURE__ */ jsx24("div", { className: "max-w-4xl mx-auto pb-4", children: isMessagesLoading ? renderMessageLoadingSkeleton() : messages.length === 0 ? renderSuggestions() : /* @__PURE__ */ jsx24(
4068
+ "div",
4069
+ {
4070
+ style: {
4071
+ height: `${virtualizer.getTotalSize()}px`,
4072
+ width: "100%",
4073
+ position: "relative"
4074
+ },
4075
+ children: virtualizer.getVirtualItems().map((virtualRow) => {
4076
+ const message = messages[virtualRow.index];
4077
+ const prevMessage = virtualRow.index > 0 ? messages[virtualRow.index - 1] : null;
4078
+ const isGrouped = prevMessage !== null && prevMessage.role === message.role;
4079
+ return /* @__PURE__ */ jsx24(
4080
+ "div",
4081
+ {
4082
+ "data-index": virtualRow.index,
4083
+ ref: virtualizer.measureElement,
4084
+ style: {
4085
+ position: "absolute",
4086
+ top: 0,
4087
+ left: 0,
4088
+ width: "100%",
4089
+ transform: `translateY(${virtualRow.start}px)`
4090
+ },
4091
+ children: /* @__PURE__ */ jsxs14("div", { className: virtualRow.index === 0 ? "" : isGrouped ? "pt-2" : "pt-4", children: [
4092
+ /* @__PURE__ */ jsx24(
4093
+ Message,
4094
+ {
4095
+ message,
4096
+ ...messageProps,
4097
+ isGrouped
4098
+ }
4099
+ ),
4100
+ message.role === "assistant" && renderInlineSuggestions(message.id)
4101
+ ] })
4102
+ },
4103
+ message.id
4104
+ );
4105
+ })
4106
+ }
4107
+ ) })
4083
4108
  }
4084
4109
  ),
4085
4110
  /* @__PURE__ */ jsx24("div", { className: "bg-background pb-[env(safe-area-inset-bottom)]", children: /* @__PURE__ */ jsx24(
@@ -4158,7 +4183,7 @@ var ChatUI = ({
4158
4183
  };
4159
4184
 
4160
4185
  // src/components/chat/ThreadManager.tsx
4161
- import { useState as useState9, useRef as useRef8, useEffect as useEffect11 } from "react";
4186
+ import { useState as useState9, useRef as useRef8, useEffect as useEffect10 } from "react";
4162
4187
  import {
4163
4188
  Plus as Plus4,
4164
4189
  MessageSquare as MessageSquare2,
@@ -4173,12 +4198,12 @@ import {
4173
4198
  X as X4,
4174
4199
  Check as Check4
4175
4200
  } from "lucide-react";
4176
- import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
4201
+ import { Fragment as Fragment6, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
4177
4202
  var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onArchive }) => {
4178
4203
  const [isEditing, setIsEditing] = useState9(false);
4179
4204
  const [editTitle, setEditTitle] = useState9(thread.title);
4180
4205
  const inputRef = useRef8(null);
4181
- useEffect11(() => {
4206
+ useEffect10(() => {
4182
4207
  if (isEditing && inputRef.current) {
4183
4208
  inputRef.current.focus();
4184
4209
  inputRef.current.select();
@@ -4218,7 +4243,7 @@ var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onAr
4218
4243
  ),
4219
4244
  /* @__PURE__ */ jsx25(Button, { size: "sm", variant: "ghost", onClick: handleSaveEdit, children: /* @__PURE__ */ jsx25(Check4, { className: "h-3 w-3" }) }),
4220
4245
  /* @__PURE__ */ jsx25(Button, { size: "sm", variant: "ghost", onClick: handleCancelEdit, children: /* @__PURE__ */ jsx25(X4, { className: "h-3 w-3" }) })
4221
- ] }) : /* @__PURE__ */ jsxs15(Fragment7, { children: [
4246
+ ] }) : /* @__PURE__ */ jsxs15(Fragment6, { children: [
4222
4247
  /* @__PURE__ */ jsx25("h4", { className: "font-medium text-sm truncate mb-1", children: thread.title }),
4223
4248
  /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
4224
4249
  /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
@@ -4231,7 +4256,7 @@ var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onAr
4231
4256
  /* @__PURE__ */ jsx25(Calendar2, { className: "h-3 w-3" }),
4232
4257
  formatDate(thread.updatedAt, config?.labels)
4233
4258
  ] }),
4234
- thread.isArchived && /* @__PURE__ */ jsxs15(Fragment7, { children: [
4259
+ thread.isArchived && /* @__PURE__ */ jsxs15(Fragment6, { children: [
4235
4260
  /* @__PURE__ */ jsx25(Separator, { orientation: "vertical", className: "h-3" }),
4236
4261
  /* @__PURE__ */ jsxs15(Badge, { variant: "secondary", className: "text-xs", children: [
4237
4262
  /* @__PURE__ */ jsx25(Archive2, { className: "h-2 w-2 mr-1" }),