@copilotz/chat-ui 0.1.7 → 0.1.9

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 text-left ${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,
@@ -911,11 +913,11 @@ var Message = memo(({
911
913
  ] }) })
912
914
  ]
913
915
  }
914
- ) });
916
+ );
915
917
  }, arePropsEqual);
916
918
 
917
919
  // src/components/chat/Sidebar.tsx
918
- import { useState as useState4, useRef as useRef5, useEffect as useEffect7 } from "react";
920
+ import { useState as useState4, useRef as useRef5, useEffect as useEffect6 } from "react";
919
921
 
920
922
  // src/components/ui/input.tsx
921
923
  import { jsx as jsx8 } from "react/jsx-runtime";
@@ -2166,7 +2168,7 @@ var Sidebar2 = ({
2166
2168
  const [editTitle, setEditTitle] = useState4("");
2167
2169
  const inputRef = useRef5(null);
2168
2170
  const { setOpen } = useSidebar();
2169
- useEffect7(() => {
2171
+ useEffect6(() => {
2170
2172
  if (editingThreadId && inputRef.current) {
2171
2173
  inputRef.current.focus();
2172
2174
  inputRef.current.select();
@@ -2583,10 +2585,10 @@ var ChatHeader = ({
2583
2585
  };
2584
2586
 
2585
2587
  // src/components/chat/ChatInput.tsx
2586
- import { useState as useState6, useRef as useRef6, useCallback as useCallback3, useEffect as useEffect9, memo as memo2 } from "react";
2588
+ import { useState as useState6, useRef as useRef6, useCallback as useCallback3, useEffect as useEffect8, memo as memo2 } from "react";
2587
2589
 
2588
2590
  // 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";
2591
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useState as useState5 } from "react";
2590
2592
  import { jsx as jsx19 } from "react/jsx-runtime";
2591
2593
  var Ctx = createContext2(void 0);
2592
2594
  var ChatUserContextProvider = ({ children, initial }) => {
@@ -2594,7 +2596,7 @@ var ChatUserContextProvider = ({ children, initial }) => {
2594
2596
  updatedAt: Date.now(),
2595
2597
  ...initial ?? {}
2596
2598
  }));
2597
- useEffect8(() => {
2599
+ useEffect7(() => {
2598
2600
  if (!initial) return;
2599
2601
  setCtx((prev) => ({
2600
2602
  ...prev,
@@ -2909,7 +2911,7 @@ var ChatInput = memo2(function ChatInput2({
2909
2911
  const recordingStartTime = useRef6(0);
2910
2912
  const recordingInterval = useRef6(null);
2911
2913
  const mediaStreamRef = useRef6(null);
2912
- useEffect9(() => {
2914
+ useEffect8(() => {
2913
2915
  return () => {
2914
2916
  if (mediaStreamRef.current) {
2915
2917
  mediaStreamRef.current.getTracks().forEach((track) => track.stop());
@@ -3180,7 +3182,7 @@ var ChatInput = memo2(function ChatInput2({
3180
3182
  onChange: (e) => onChange(e.target.value),
3181
3183
  onKeyDown: handleKeyDown,
3182
3184
  placeholder,
3183
- disabled: disabled || isGenerating,
3185
+ disabled,
3184
3186
  className: "max-h-[120px] resize-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0",
3185
3187
  rows: 1
3186
3188
  }
@@ -3709,7 +3711,7 @@ var UserProfile = ({
3709
3711
 
3710
3712
  // src/components/chat/ChatUI.tsx
3711
3713
  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";
3714
+ import { jsx as jsx24, jsxs as jsxs14 } from "react/jsx-runtime";
3713
3715
  var ChatUI = ({
3714
3716
  messages = [],
3715
3717
  threads = [],
@@ -3766,35 +3768,40 @@ var ChatUI = ({
3766
3768
  isSidebarCollapsed: false
3767
3769
  // No longer used for main sidebar
3768
3770
  });
3769
- useEffect10(() => {
3771
+ useEffect9(() => {
3770
3772
  if (currentThreadId !== state.selectedThreadId) {
3771
3773
  setState((prev) => ({ ...prev, selectedThreadId: currentThreadId }));
3772
3774
  }
3773
3775
  }, [currentThreadId]);
3774
3776
  const initialInputApplied = useRef7(false);
3775
3777
  const initialInputConsumedRef = useRef7(false);
3776
- useEffect10(() => {
3778
+ useEffect9(() => {
3777
3779
  if (initialInput && !initialInputApplied.current) {
3778
3780
  setInputValue(initialInput);
3779
3781
  initialInputApplied.current = true;
3780
3782
  }
3781
3783
  }, [initialInput]);
3782
- const messagesEndRef = useRef7(null);
3783
3784
  const scrollAreaRef = useRef7(null);
3784
3785
  const stateRef = useRef7(state);
3785
3786
  const inputValueRef = useRef7(inputValue);
3786
3787
  const attachmentsRef = useRef7(attachments);
3787
- useEffect10(() => {
3788
+ useEffect9(() => {
3788
3789
  stateRef.current = state;
3789
3790
  }, [state]);
3790
- useEffect10(() => {
3791
+ useEffect9(() => {
3791
3792
  inputValueRef.current = inputValue;
3792
3793
  }, [inputValue]);
3793
- useEffect10(() => {
3794
+ useEffect9(() => {
3794
3795
  attachmentsRef.current = attachments;
3795
3796
  }, [attachments]);
3796
3797
  const [isCustomMounted, setIsCustomMounted] = useState8(false);
3797
3798
  const [isCustomVisible, setIsCustomVisible] = useState8(false);
3799
+ const virtualizer = useVirtualizer({
3800
+ count: messages.length,
3801
+ getScrollElement: () => scrollAreaRef.current,
3802
+ estimateSize: () => 100,
3803
+ overscan: 5
3804
+ });
3798
3805
  const createStateCallback = useCallback4(
3799
3806
  (setter) => ({
3800
3807
  setState: (newState) => setter?.(newState),
@@ -3807,7 +3814,7 @@ var ChatUI = ({
3807
3814
  []
3808
3815
  // No dependencies - uses refs for latest state
3809
3816
  );
3810
- useEffect10(() => {
3817
+ useEffect9(() => {
3811
3818
  const checkMobile = () => {
3812
3819
  setIsMobile(globalThis.innerWidth < 1024);
3813
3820
  };
@@ -3815,7 +3822,7 @@ var ChatUI = ({
3815
3822
  globalThis.addEventListener("resize", checkMobile);
3816
3823
  return () => globalThis.removeEventListener("resize", checkMobile);
3817
3824
  }, []);
3818
- useEffect10(() => {
3825
+ useEffect9(() => {
3819
3826
  if (!isMobile || !config.customComponent?.component) return;
3820
3827
  if (state.showSidebar) {
3821
3828
  setIsCustomMounted(true);
@@ -3826,21 +3833,40 @@ var ChatUI = ({
3826
3833
  return () => clearTimeout(t);
3827
3834
  }
3828
3835
  }, [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;
3836
+ const prevMessageCountRef = useRef7(0);
3837
+ useEffect9(() => {
3838
+ if (messages.length === 0) {
3839
+ prevMessageCountRef.current = 0;
3840
+ return;
3838
3841
  }
3839
- }, [messages, state.isAtBottom]);
3842
+ const wasEmpty = prevMessageCountRef.current === 0;
3843
+ prevMessageCountRef.current = messages.length;
3844
+ if (wasEmpty) {
3845
+ requestAnimationFrame(() => {
3846
+ requestAnimationFrame(() => {
3847
+ virtualizer.scrollToIndex(messages.length - 1, { align: "end" });
3848
+ });
3849
+ });
3850
+ return;
3851
+ }
3852
+ if (!state.isAtBottom) return;
3853
+ requestAnimationFrame(() => {
3854
+ const viewport = scrollAreaRef.current;
3855
+ if (!viewport) return;
3856
+ try {
3857
+ viewport.scrollTo({ top: viewport.scrollHeight, behavior: "smooth" });
3858
+ } catch {
3859
+ viewport.scrollTop = viewport.scrollHeight;
3860
+ }
3861
+ });
3862
+ }, [messages, state.isAtBottom, virtualizer]);
3840
3863
  const handleScroll = useCallback4((e) => {
3841
3864
  const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
3842
3865
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
3843
- setState((prev) => ({ ...prev, isAtBottom }));
3866
+ setState((prev) => {
3867
+ if (prev.isAtBottom === isAtBottom) return prev;
3868
+ return { ...prev, isAtBottom };
3869
+ });
3844
3870
  }, []);
3845
3871
  const handleSendMessage = useCallback4((content, messageAttachments = []) => {
3846
3872
  if (!content.trim() && messageAttachments.length === 0) return;
@@ -3961,49 +3987,26 @@ var ChatUI = ({
3961
3987
  `message-skeleton-${index}`
3962
3988
  );
3963
3989
  }) });
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,
3990
+ const messageProps = useMemo3(() => ({
3991
+ userAvatar: user?.avatar,
3992
+ userName: user?.name,
3993
+ assistantAvatar: assistant?.avatar,
3994
+ assistantName: assistant?.name,
3995
+ showTimestamp: config.ui.showTimestamps,
3996
+ showAvatar: config.ui.showAvatars,
3997
+ enableCopy: config.features.enableMessageCopy,
3998
+ enableEdit: config.features.enableMessageEditing,
3999
+ enableRegenerate: config.features.enableRegeneration,
4000
+ enableToolCallsDisplay: config.features.enableToolCallsDisplay,
4001
+ compactMode: config.ui.compactMode,
4002
+ onAction: handleMessageAction,
4003
+ toolUsedLabel: config.labels.toolUsed,
4004
+ thinkingLabel: config.labels.thinking
4005
+ }), [
4001
4006
  user?.avatar,
4002
4007
  user?.name,
4003
4008
  assistant?.avatar,
4004
4009
  assistant?.name,
4005
- config.branding.title,
4006
- config.branding.subtitle,
4007
4010
  config.ui.showTimestamps,
4008
4011
  config.ui.showAvatars,
4009
4012
  config.ui.compactMode,
@@ -4013,9 +4016,7 @@ var ChatUI = ({
4013
4016
  config.features.enableToolCallsDisplay,
4014
4017
  config.labels.toolUsed,
4015
4018
  config.labels.thinking,
4016
- handleMessageAction,
4017
- messageSuggestions,
4018
- suggestions
4019
+ handleMessageAction
4019
4020
  ]);
4020
4021
  const shouldShowAgentSelector = Boolean(
4021
4022
  config.agentSelector?.enabled && onSelectAgent && agentOptions.length > 0 && (!config.agentSelector?.hideIfSingle || agentOptions.length > 1)
@@ -4076,10 +4077,48 @@ var ChatUI = ({
4076
4077
  className: "flex-1 min-h-0",
4077
4078
  viewportClassName: "p-4 overscroll-contain",
4078
4079
  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
- ] })
4080
+ style: { contain: "strict" },
4081
+ children: /* @__PURE__ */ jsx24("div", { className: "max-w-4xl mx-auto pb-4", children: isMessagesLoading ? renderMessageLoadingSkeleton() : messages.length === 0 ? renderSuggestions() : /* @__PURE__ */ jsx24(
4082
+ "div",
4083
+ {
4084
+ style: {
4085
+ height: `${virtualizer.getTotalSize()}px`,
4086
+ width: "100%",
4087
+ position: "relative"
4088
+ },
4089
+ children: virtualizer.getVirtualItems().map((virtualRow) => {
4090
+ const message = messages[virtualRow.index];
4091
+ const prevMessage = virtualRow.index > 0 ? messages[virtualRow.index - 1] : null;
4092
+ const isGrouped = prevMessage !== null && prevMessage.role === message.role;
4093
+ return /* @__PURE__ */ jsx24(
4094
+ "div",
4095
+ {
4096
+ "data-index": virtualRow.index,
4097
+ ref: virtualizer.measureElement,
4098
+ style: {
4099
+ position: "absolute",
4100
+ top: 0,
4101
+ left: 0,
4102
+ width: "100%",
4103
+ transform: `translateY(${virtualRow.start}px)`
4104
+ },
4105
+ children: /* @__PURE__ */ jsxs14("div", { className: virtualRow.index === 0 ? "" : isGrouped ? "pt-2" : "pt-4", children: [
4106
+ /* @__PURE__ */ jsx24(
4107
+ Message,
4108
+ {
4109
+ message,
4110
+ ...messageProps,
4111
+ isGrouped
4112
+ }
4113
+ ),
4114
+ message.role === "assistant" && renderInlineSuggestions(message.id)
4115
+ ] })
4116
+ },
4117
+ message.id
4118
+ );
4119
+ })
4120
+ }
4121
+ ) })
4083
4122
  }
4084
4123
  ),
4085
4124
  /* @__PURE__ */ jsx24("div", { className: "bg-background pb-[env(safe-area-inset-bottom)]", children: /* @__PURE__ */ jsx24(
@@ -4158,7 +4197,7 @@ var ChatUI = ({
4158
4197
  };
4159
4198
 
4160
4199
  // src/components/chat/ThreadManager.tsx
4161
- import { useState as useState9, useRef as useRef8, useEffect as useEffect11 } from "react";
4200
+ import { useState as useState9, useRef as useRef8, useEffect as useEffect10 } from "react";
4162
4201
  import {
4163
4202
  Plus as Plus4,
4164
4203
  MessageSquare as MessageSquare2,
@@ -4173,12 +4212,12 @@ import {
4173
4212
  X as X4,
4174
4213
  Check as Check4
4175
4214
  } from "lucide-react";
4176
- import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
4215
+ import { Fragment as Fragment6, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
4177
4216
  var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onArchive }) => {
4178
4217
  const [isEditing, setIsEditing] = useState9(false);
4179
4218
  const [editTitle, setEditTitle] = useState9(thread.title);
4180
4219
  const inputRef = useRef8(null);
4181
- useEffect11(() => {
4220
+ useEffect10(() => {
4182
4221
  if (isEditing && inputRef.current) {
4183
4222
  inputRef.current.focus();
4184
4223
  inputRef.current.select();
@@ -4218,7 +4257,7 @@ var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onAr
4218
4257
  ),
4219
4258
  /* @__PURE__ */ jsx25(Button, { size: "sm", variant: "ghost", onClick: handleSaveEdit, children: /* @__PURE__ */ jsx25(Check4, { className: "h-3 w-3" }) }),
4220
4259
  /* @__PURE__ */ jsx25(Button, { size: "sm", variant: "ghost", onClick: handleCancelEdit, children: /* @__PURE__ */ jsx25(X4, { className: "h-3 w-3" }) })
4221
- ] }) : /* @__PURE__ */ jsxs15(Fragment7, { children: [
4260
+ ] }) : /* @__PURE__ */ jsxs15(Fragment6, { children: [
4222
4261
  /* @__PURE__ */ jsx25("h4", { className: "font-medium text-sm truncate mb-1", children: thread.title }),
4223
4262
  /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
4224
4263
  /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1", children: [
@@ -4231,7 +4270,7 @@ var ThreadItem = ({ thread, isActive, config, onSelect, onRename, onDelete, onAr
4231
4270
  /* @__PURE__ */ jsx25(Calendar2, { className: "h-3 w-3" }),
4232
4271
  formatDate(thread.updatedAt, config?.labels)
4233
4272
  ] }),
4234
- thread.isArchived && /* @__PURE__ */ jsxs15(Fragment7, { children: [
4273
+ thread.isArchived && /* @__PURE__ */ jsxs15(Fragment6, { children: [
4235
4274
  /* @__PURE__ */ jsx25(Separator, { orientation: "vertical", className: "h-3" }),
4236
4275
  /* @__PURE__ */ jsxs15(Badge, { variant: "secondary", className: "text-xs", children: [
4237
4276
  /* @__PURE__ */ jsx25(Archive2, { className: "h-2 w-2 mr-1" }),