@connectycube/react-ui-kit 0.0.19 → 0.0.22
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/configs/dependencies.json +21 -0
- package/configs/imports.json +7 -0
- package/dist/index.cjs +1 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -35
- package/dist/index.js.map +1 -1
- package/dist/types/components/attachment.d.ts +7 -8
- package/dist/types/components/attachment.d.ts.map +1 -1
- package/dist/types/components/avatar.d.ts +1 -0
- package/dist/types/components/avatar.d.ts.map +1 -1
- package/dist/types/components/badge.d.ts +1 -1
- package/dist/types/components/button.d.ts +2 -2
- package/dist/types/components/chat-bubble.d.ts +32 -0
- package/dist/types/components/chat-bubble.d.ts.map +1 -0
- package/dist/types/components/chat-input.d.ts +27 -0
- package/dist/types/components/chat-input.d.ts.map +1 -0
- package/dist/types/components/chat-list.d.ts +30 -0
- package/dist/types/components/chat-list.d.ts.map +1 -0
- package/dist/types/components/checkbox.d.ts +11 -0
- package/dist/types/components/checkbox.d.ts.map +1 -0
- package/dist/types/components/dialog-item.d.ts.map +1 -1
- package/dist/types/components/dialogs-list.d.ts +14 -0
- package/dist/types/components/dialogs-list.d.ts.map +1 -0
- package/dist/types/components/file-picker.d.ts +1 -1
- package/dist/types/components/file-picker.d.ts.map +1 -1
- package/dist/types/components/linkify-text.d.ts +6 -1
- package/dist/types/components/linkify-text.d.ts.map +1 -1
- package/dist/types/components/placeholder-text.d.ts.map +1 -1
- package/dist/types/components/quick-actions.d.ts +14 -0
- package/dist/types/components/quick-actions.d.ts.map +1 -0
- package/dist/types/components/status-call.d.ts +8 -0
- package/dist/types/components/status-call.d.ts.map +1 -0
- package/dist/types/components/switch.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/gen/components/attachment.jsx +27 -25
- package/gen/components/avatar.jsx +14 -2
- package/gen/components/button.jsx +1 -1
- package/gen/components/chat-bubble.jsx +141 -0
- package/gen/components/chat-input.jsx +152 -0
- package/gen/components/chat-list.jsx +151 -0
- package/gen/components/checkbox.jsx +30 -0
- package/gen/components/dialog-item.jsx +5 -2
- package/gen/components/dialogs-list.jsx +73 -0
- package/gen/components/dismiss-layer.jsx +1 -1
- package/gen/components/file-picker.jsx +2 -2
- package/gen/components/linkify-text.jsx +41 -2
- package/gen/components/placeholder-text.jsx +5 -1
- package/gen/components/quick-actions.jsx +62 -0
- package/gen/components/search.jsx +1 -1
- package/gen/components/status-call.jsx +18 -0
- package/gen/components/stream-view.jsx +8 -8
- package/gen/components/switch.jsx +0 -2
- package/gen/index.js +16 -0
- package/package.json +17 -13
- package/src/components/attachment.tsx +38 -37
- package/src/components/avatar.tsx +3 -1
- package/src/components/button.tsx +1 -1
- package/src/components/chat-bubble.tsx +176 -0
- package/src/components/chat-input.tsx +172 -0
- package/src/components/chat-list.tsx +164 -0
- package/src/components/checkbox.tsx +40 -0
- package/src/components/connectycube-ui/attachment.tsx +269 -0
- package/src/components/connectycube-ui/chat-input.tsx +174 -0
- package/src/components/connectycube-ui/chat-message.tsx +138 -0
- package/src/components/connectycube-ui/link-preview.tsx +149 -0
- package/src/components/dialog-item.tsx +5 -2
- package/src/components/dialogs-list.tsx +84 -0
- package/src/components/dismiss-layer.tsx +1 -1
- package/src/components/file-picker.tsx +3 -3
- package/src/components/linkify-text.tsx +44 -3
- package/src/components/placeholder-text.tsx +5 -1
- package/src/components/quick-actions.tsx +74 -0
- package/src/components/search.tsx +1 -1
- package/src/components/status-call.tsx +23 -0
- package/src/components/stream-view.tsx +8 -8
- package/src/components/switch.tsx +0 -2
- package/src/index.ts +21 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { useInView } from 'react-intersection-observer';
|
|
3
|
+
import { Avatar } from './avatar';
|
|
4
|
+
import { FormattedDate } from './formatted-date';
|
|
5
|
+
import { StatusSent } from './status-sent';
|
|
6
|
+
import { cn } from './utils';
|
|
7
|
+
|
|
8
|
+
function ChatBubbleBase({ onView = () => {}, isLast, children, ...props }, ref) {
|
|
9
|
+
const [setRef, inView] = useInView();
|
|
10
|
+
const messageRef = useRef(null);
|
|
11
|
+
const setRefs = useCallback(
|
|
12
|
+
(node) => {
|
|
13
|
+
messageRef.current = node;
|
|
14
|
+
setRef(node);
|
|
15
|
+
},
|
|
16
|
+
[setRef]
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (inView) {
|
|
21
|
+
onView();
|
|
22
|
+
}
|
|
23
|
+
}, [inView, onView]);
|
|
24
|
+
|
|
25
|
+
useImperativeHandle(ref, () => messageRef.current || {}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div ref={setRefs} {...props} className={cn('mt-2', isLast && 'mb-2', inView && 'view', props?.className)}>
|
|
29
|
+
{children}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ChatBubble = forwardRef(ChatBubbleBase);
|
|
35
|
+
|
|
36
|
+
function ChatBubbleMessageBase(
|
|
37
|
+
{
|
|
38
|
+
fromMe,
|
|
39
|
+
sameSenderAbove,
|
|
40
|
+
title,
|
|
41
|
+
senderName,
|
|
42
|
+
senderAvatar,
|
|
43
|
+
date = new Date(),
|
|
44
|
+
language = 'en',
|
|
45
|
+
statusSent,
|
|
46
|
+
avatarProps,
|
|
47
|
+
bubbleProps,
|
|
48
|
+
titleProps,
|
|
49
|
+
formattedDateProps,
|
|
50
|
+
statusSentProps,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
},
|
|
54
|
+
ref
|
|
55
|
+
) {
|
|
56
|
+
const hasAvatar = Boolean((avatarProps || senderAvatar) && !sameSenderAbove);
|
|
57
|
+
const hasAvatarMargin = Boolean((avatarProps || senderAvatar) && sameSenderAbove);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ChatBubble
|
|
61
|
+
ref={ref}
|
|
62
|
+
{...props}
|
|
63
|
+
className={cn(
|
|
64
|
+
`flex relative text-left whitespace-pre-wrap`,
|
|
65
|
+
fromMe ? 'self-end flex-row-reverse ml-12' : `self-start mr-12`,
|
|
66
|
+
sameSenderAbove && 'mt-1'
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{hasAvatar && (
|
|
70
|
+
<Avatar
|
|
71
|
+
name={senderName}
|
|
72
|
+
src={senderAvatar}
|
|
73
|
+
imageProps={{
|
|
74
|
+
className: 'bg-blue-200',
|
|
75
|
+
}}
|
|
76
|
+
fallbackProps={{
|
|
77
|
+
className: 'bg-blue-200',
|
|
78
|
+
}}
|
|
79
|
+
{...avatarProps}
|
|
80
|
+
className={cn('mt-1 mr-1', avatarProps?.className)}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
'relative flex flex-col min-w-42 max-w-120 rounded-xl px-2 pt-2 pb-6 shadow-sm',
|
|
87
|
+
fromMe ? 'bg-gray-200' : 'bg-blue-200',
|
|
88
|
+
hasAvatarMargin && 'ml-9',
|
|
89
|
+
bubbleProps?.className
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
{(title || senderName) && (
|
|
93
|
+
<span
|
|
94
|
+
{...titleProps}
|
|
95
|
+
className={cn(
|
|
96
|
+
'font-semibold',
|
|
97
|
+
title && 'mb-1 py-1.5 text-xs text-muted-foreground italic border-b',
|
|
98
|
+
titleProps?.className
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
101
|
+
{title || senderName}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
{children}
|
|
105
|
+
<div className="absolute bottom-1 right-2 flex items-center gap-1 italic">
|
|
106
|
+
<FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
|
|
107
|
+
<StatusSent status={statusSent} {...statusSentProps} />
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</ChatBubble>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ChatBubbleMessage = memo(forwardRef(ChatBubbleMessageBase));
|
|
115
|
+
|
|
116
|
+
ChatBubbleMessage.displayName = 'ChatBubbleMessage';
|
|
117
|
+
|
|
118
|
+
function ChatBubbleInfoBase(
|
|
119
|
+
{ info = '', iconElement, date = new Date(), language = 'en', infoProps, formattedDateProps, ...props },
|
|
120
|
+
ref
|
|
121
|
+
) {
|
|
122
|
+
return (
|
|
123
|
+
<ChatBubble
|
|
124
|
+
ref={ref}
|
|
125
|
+
{...props}
|
|
126
|
+
className={cn('flex items-center justify-center gap-2 rounded-full w-fit bg-ring/20 mx-auto px-3 py-1.5 mt-2')}
|
|
127
|
+
>
|
|
128
|
+
{iconElement}
|
|
129
|
+
<span {...infoProps} className={cn('text-sm mb-px', infoProps?.className)}>
|
|
130
|
+
{info}
|
|
131
|
+
</span>
|
|
132
|
+
<FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
|
|
133
|
+
</ChatBubble>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ChatBubbleInfo = memo(forwardRef(ChatBubbleInfoBase));
|
|
138
|
+
|
|
139
|
+
ChatBubbleInfo.displayName = 'ChatBubbleInfo';
|
|
140
|
+
|
|
141
|
+
export { ChatBubbleMessage, ChatBubbleInfo };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import TextareaAutosize from 'react-textarea-autosize';
|
|
3
|
+
import { SendHorizontal } from 'lucide-react';
|
|
4
|
+
import { Label } from './label';
|
|
5
|
+
import { cn } from './utils';
|
|
6
|
+
|
|
7
|
+
function ChatInputSendBase({ onSend = () => {}, iconElement, iconProps, ...props }, ref) {
|
|
8
|
+
return (
|
|
9
|
+
<Label
|
|
10
|
+
ref={ref}
|
|
11
|
+
{...props}
|
|
12
|
+
onClick={onSend}
|
|
13
|
+
className={cn('group rounded-full py-2 pl-2.5 pr-1.5 cursor-pointer bg-ring/90 hover:bg-ring', props?.className)}
|
|
14
|
+
>
|
|
15
|
+
{iconElement || (
|
|
16
|
+
<SendHorizontal
|
|
17
|
+
{...iconProps}
|
|
18
|
+
className={cn(
|
|
19
|
+
'text-background group-hover:scale-110 transition-all duration-200 ease-out',
|
|
20
|
+
iconProps?.className
|
|
21
|
+
)}
|
|
22
|
+
/>
|
|
23
|
+
)}
|
|
24
|
+
</Label>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ChatInputSend = forwardRef(ChatInputSendBase);
|
|
29
|
+
|
|
30
|
+
ChatInputSend.displayName = 'ChatInputSend';
|
|
31
|
+
|
|
32
|
+
function ChatInputBase(
|
|
33
|
+
{
|
|
34
|
+
pending = false,
|
|
35
|
+
draft,
|
|
36
|
+
onSend = () => {},
|
|
37
|
+
onDraft = () => {},
|
|
38
|
+
onTyping = () => {},
|
|
39
|
+
onHeightGrow = () => {},
|
|
40
|
+
hideChatInputSend = false,
|
|
41
|
+
chatInputSendProps,
|
|
42
|
+
containerProps,
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) {
|
|
48
|
+
const [value, setValue] = useState();
|
|
49
|
+
const textareaRef = useRef(null);
|
|
50
|
+
const textareaHeightRef = useRef(0);
|
|
51
|
+
const typingRef = useRef(false);
|
|
52
|
+
const typingTimeoutRef = useRef(undefined);
|
|
53
|
+
const handleStopTyping = () => {
|
|
54
|
+
typingRef.current = false;
|
|
55
|
+
onTyping(false);
|
|
56
|
+
|
|
57
|
+
if (typingTimeoutRef.current) {
|
|
58
|
+
clearTimeout(typingTimeoutRef.current);
|
|
59
|
+
typingTimeoutRef.current = undefined;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const handleOnChange = (event) => {
|
|
63
|
+
const { data } = event.nativeEvent;
|
|
64
|
+
|
|
65
|
+
setValue(event.target.value);
|
|
66
|
+
|
|
67
|
+
if (!typingRef.current && typeof data === 'string' && data.length > 0) {
|
|
68
|
+
typingRef.current = true;
|
|
69
|
+
onTyping(true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
clearTimeout(typingTimeoutRef.current);
|
|
73
|
+
typingTimeoutRef.current = setTimeout(handleStopTyping, 5000);
|
|
74
|
+
};
|
|
75
|
+
const handleOnSend = async () => {
|
|
76
|
+
handleStopTyping();
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
79
|
+
onSend(value.trim());
|
|
80
|
+
setValue('');
|
|
81
|
+
textareaRef.current?.focus();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const handleOnHeightChange = (height, meta) => {
|
|
85
|
+
if (!height && !meta) return;
|
|
86
|
+
|
|
87
|
+
if (height !== textareaHeightRef.current) {
|
|
88
|
+
const shift = height > textareaHeightRef.current ? meta.rowHeight : -meta.rowHeight;
|
|
89
|
+
|
|
90
|
+
onHeightGrow({
|
|
91
|
+
height,
|
|
92
|
+
shift,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
textareaHeightRef.current = height;
|
|
97
|
+
};
|
|
98
|
+
const handleOnKeyDown = (event) => {
|
|
99
|
+
if (event.key === 'Enter' && event.shiftKey === false) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
props.onKeyDown?.(event);
|
|
102
|
+
handleOnSend();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const textarea = textareaRef.current;
|
|
108
|
+
|
|
109
|
+
handleStopTyping();
|
|
110
|
+
queueMicrotask(() => setValue(draft));
|
|
111
|
+
textarea?.focus();
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
onDraft(textarea?.value);
|
|
115
|
+
};
|
|
116
|
+
}, [props.key]);
|
|
117
|
+
|
|
118
|
+
useImperativeHandle(ref, () => textareaRef.current || {}, []);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div {...containerProps} className={cn('flex items-end gap-2', containerProps?.className)}>
|
|
122
|
+
<TextareaAutosize
|
|
123
|
+
name="textarea-autosize"
|
|
124
|
+
ref={textareaRef}
|
|
125
|
+
autoFocus
|
|
126
|
+
minRows={1}
|
|
127
|
+
maxRows={8}
|
|
128
|
+
{...props}
|
|
129
|
+
value={value}
|
|
130
|
+
onChange={handleOnChange}
|
|
131
|
+
onKeyDown={handleOnKeyDown}
|
|
132
|
+
onHeightChange={handleOnHeightChange}
|
|
133
|
+
className={cn(
|
|
134
|
+
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-11 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
135
|
+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring',
|
|
136
|
+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
137
|
+
'resize-none py-2.5',
|
|
138
|
+
pending && 'pointer-events-none bg-muted-foreground border-ring ring-ring/50 animate-pulse',
|
|
139
|
+
props?.className
|
|
140
|
+
)}
|
|
141
|
+
/>
|
|
142
|
+
{!hideChatInputSend && <ChatInputSend onSend={handleOnSend} {...chatInputSendProps} />}
|
|
143
|
+
{children}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ChatInput = forwardRef(ChatInputBase);
|
|
149
|
+
|
|
150
|
+
ChatInput.displayName = 'ChatInput';
|
|
151
|
+
|
|
152
|
+
export { ChatInput };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, useCallback } from 'react';
|
|
2
|
+
import { Virtualizer } from 'virtua';
|
|
3
|
+
import { FilePickerDropzone } from './file-picker';
|
|
4
|
+
import { QuickActions } from './quick-actions';
|
|
5
|
+
import { Spinner } from './spinner';
|
|
6
|
+
import { cn } from './utils';
|
|
7
|
+
|
|
8
|
+
function DefaultChatListWrapper({ children, ...props }) {
|
|
9
|
+
return (
|
|
10
|
+
<div {...props} className={cn('relative size-full min-h-0', props?.className)}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ChatListBase(
|
|
17
|
+
{
|
|
18
|
+
key,
|
|
19
|
+
loading = false,
|
|
20
|
+
textareaMeasurement,
|
|
21
|
+
offsetToReach = 200,
|
|
22
|
+
onScrollStartReached,
|
|
23
|
+
onScrollEndReached,
|
|
24
|
+
onListReset,
|
|
25
|
+
onListCreate,
|
|
26
|
+
onListGrow,
|
|
27
|
+
enableFilePickerDropzone = true,
|
|
28
|
+
containerProps,
|
|
29
|
+
filePickerDropzoneProps,
|
|
30
|
+
quickActionsProps,
|
|
31
|
+
children,
|
|
32
|
+
quickActionsVisible,
|
|
33
|
+
minItemsCount = 1,
|
|
34
|
+
...props
|
|
35
|
+
},
|
|
36
|
+
ref
|
|
37
|
+
) {
|
|
38
|
+
const ChatListWrapper = enableFilePickerDropzone ? FilePickerDropzone : DefaultChatListWrapper;
|
|
39
|
+
const chatListWrapperProps = enableFilePickerDropzone ? filePickerDropzoneProps : containerProps;
|
|
40
|
+
const itemsCount = Array.isArray(children) ? children.length : Array.isArray(props.data) ? props.data.length : 0;
|
|
41
|
+
const itemsCountRef = useRef(0);
|
|
42
|
+
const keyRef = useRef(undefined);
|
|
43
|
+
const [shouldPrependMessages, setShouldPrependMessages] = useState(false);
|
|
44
|
+
const [isPreparing, setIsPreparing] = useState(false);
|
|
45
|
+
const virtuaRef = useRef(null);
|
|
46
|
+
const prepareNextVirtualizer = () => {
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
setIsPreparing(true);
|
|
49
|
+
queueMicrotask(() => setIsPreparing(false));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const handleOnScroll = async (offset) => {
|
|
53
|
+
props.onScroll?.(offset);
|
|
54
|
+
|
|
55
|
+
if (!virtuaRef.current) return;
|
|
56
|
+
|
|
57
|
+
if (typeof onScrollStartReached === 'function' && offset < offsetToReach) {
|
|
58
|
+
setShouldPrependMessages(true);
|
|
59
|
+
onScrollStartReached();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
typeof onScrollEndReached === 'function' &&
|
|
64
|
+
virtuaRef.current.viewportSize + offset + offsetToReach > virtuaRef.current.scrollSize
|
|
65
|
+
) {
|
|
66
|
+
onScrollEndReached();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const scrollToBottom = useCallback(
|
|
70
|
+
(force = false) => {
|
|
71
|
+
if (!virtuaRef.current) return;
|
|
72
|
+
|
|
73
|
+
if (force || virtuaRef.current.scrollSize - virtuaRef.current.scrollOffset < virtuaRef.current.viewportSize * 2) {
|
|
74
|
+
queueMicrotask(() =>
|
|
75
|
+
virtuaRef.current?.scrollToIndex(itemsCount - 1, {
|
|
76
|
+
align: 'start',
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[itemsCount]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (key !== keyRef.current) {
|
|
86
|
+
prepareNextVirtualizer();
|
|
87
|
+
onListReset?.(keyRef.current, key);
|
|
88
|
+
keyRef.current = key;
|
|
89
|
+
itemsCountRef.current = 0;
|
|
90
|
+
}
|
|
91
|
+
}, [key, onListReset]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (itemsCountRef.current === 0 && itemsCount > 0) {
|
|
95
|
+
prepareNextVirtualizer();
|
|
96
|
+
onListCreate?.();
|
|
97
|
+
itemsCountRef.current = itemsCount;
|
|
98
|
+
scrollToBottom(true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (itemsCount > itemsCountRef.current) {
|
|
102
|
+
onListGrow?.();
|
|
103
|
+
itemsCountRef.current = itemsCount;
|
|
104
|
+
|
|
105
|
+
if (shouldPrependMessages) {
|
|
106
|
+
queueMicrotask(() => setShouldPrependMessages(false));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
scrollToBottom();
|
|
110
|
+
}
|
|
111
|
+
}, [itemsCount, shouldPrependMessages, onListCreate, onListGrow, scrollToBottom]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
virtuaRef.current?.scrollBy(textareaMeasurement.shift);
|
|
115
|
+
}, [textareaMeasurement]);
|
|
116
|
+
|
|
117
|
+
useImperativeHandle(
|
|
118
|
+
ref,
|
|
119
|
+
() => ({
|
|
120
|
+
...(virtuaRef.current || {}),
|
|
121
|
+
scrollToBottom,
|
|
122
|
+
}),
|
|
123
|
+
[scrollToBottom]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (isPreparing) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (minItemsCount >= itemsCount) {
|
|
131
|
+
return quickActionsVisible ? <QuickActions {...quickActionsProps} /> : <Spinner loading layout="centered" />;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<ChatListWrapper {...chatListWrapperProps}>
|
|
136
|
+
<Spinner loading={loading} className="my-5" />
|
|
137
|
+
<div className="flex flex-col h-full overflow-y-auto px-2">
|
|
138
|
+
<div className="grow" />
|
|
139
|
+
<Virtualizer ref={virtuaRef} onScroll={handleOnScroll} shift={shouldPrependMessages} {...props}>
|
|
140
|
+
{children}
|
|
141
|
+
</Virtualizer>
|
|
142
|
+
</div>
|
|
143
|
+
</ChatListWrapper>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ChatList = forwardRef(ChatListBase);
|
|
148
|
+
|
|
149
|
+
ChatList.displayName = 'ChatList';
|
|
150
|
+
|
|
151
|
+
export { ChatList };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
3
|
+
import { CheckIcon } from 'lucide-react';
|
|
4
|
+
import { cn } from './utils';
|
|
5
|
+
|
|
6
|
+
function CheckboxBase({ iconElement, iconProps, indicatorProps, ...props }, ref) {
|
|
7
|
+
return (
|
|
8
|
+
<CheckboxPrimitive.Root
|
|
9
|
+
ref={ref}
|
|
10
|
+
{...props}
|
|
11
|
+
className={cn(
|
|
12
|
+
'peer border-input dark:bg-input/30 data-[state=checked]:bg-ring data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-ring data-[state=checked]:border-ring focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
|
13
|
+
props?.className
|
|
14
|
+
)}
|
|
15
|
+
>
|
|
16
|
+
<CheckboxPrimitive.Indicator
|
|
17
|
+
{...indicatorProps}
|
|
18
|
+
className={cn('flex items-center justify-center text-current transition-none', indicatorProps?.className)}
|
|
19
|
+
>
|
|
20
|
+
{iconElement || <CheckIcon {...iconProps} className={cn('size-3.5', iconProps?.className)} />}
|
|
21
|
+
</CheckboxPrimitive.Indicator>
|
|
22
|
+
</CheckboxPrimitive.Root>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Checkbox = forwardRef(CheckboxBase);
|
|
27
|
+
|
|
28
|
+
Checkbox.displayName = 'Checkbox';
|
|
29
|
+
|
|
30
|
+
export { Checkbox };
|
|
@@ -60,7 +60,7 @@ function DialogItemBase(
|
|
|
60
60
|
onClick={onSelect}
|
|
61
61
|
className={cn(
|
|
62
62
|
'flex items-start gap-2 px-2 flex-1 cursor-pointer',
|
|
63
|
-
'transition-colors duration-200 ease-
|
|
63
|
+
'transition-colors duration-200 ease-out',
|
|
64
64
|
`${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
|
|
65
65
|
props?.className
|
|
66
66
|
)}
|
|
@@ -98,7 +98,10 @@ function DialogItemBase(
|
|
|
98
98
|
<div {...footerProps} className={cn('flex items-start justify-between gap-1', footerProps?.className)}>
|
|
99
99
|
<span
|
|
100
100
|
{...lastMessageProps}
|
|
101
|
-
className={cn(
|
|
101
|
+
className={cn(
|
|
102
|
+
'text-sm text-left text-muted-foreground break-all line-clamp-2',
|
|
103
|
+
lastMessageProps?.className
|
|
104
|
+
)}
|
|
102
105
|
>
|
|
103
106
|
{typingStatusText ||
|
|
104
107
|
(draft ? (
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { VList } from 'virtua';
|
|
3
|
+
import { PlaceholderText } from './placeholder-text';
|
|
4
|
+
|
|
5
|
+
const PendingItem = () => (
|
|
6
|
+
<div className="flex flex-row gap-2 mx-2 border-b">
|
|
7
|
+
<div className="size-13 my-2 rounded-full bg-muted animate-pulse" />
|
|
8
|
+
<div className="flex-1 text-muted">
|
|
9
|
+
<div className="flex flex-row items-center justify-between h-6">
|
|
10
|
+
<div className="w-2/3 h-4 rounded-full bg-muted animate-pulse" />
|
|
11
|
+
<div className="w-1/6 h-3.5 rounded-full bg-muted animate-pulse" />
|
|
12
|
+
</div>
|
|
13
|
+
<div className="flex flex-col items-start justify-around h-10">
|
|
14
|
+
<div className="w-7/8 h-3.5 rounded-full bg-muted animate-pulse" />
|
|
15
|
+
<div className="w-5/6 h-3.5 rounded-full bg-muted animate-pulse" />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function DialogsListBase(
|
|
22
|
+
{
|
|
23
|
+
children,
|
|
24
|
+
pending,
|
|
25
|
+
pendingListLength = 5,
|
|
26
|
+
offsetToReach = 50,
|
|
27
|
+
placeholderVisible,
|
|
28
|
+
placeholderTitles = ['No dialogs yet.', 'Start a conversation!'],
|
|
29
|
+
onScrollStartReached,
|
|
30
|
+
onScrollEndReached,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref
|
|
34
|
+
) {
|
|
35
|
+
const vListRef = useRef(null);
|
|
36
|
+
const skeletonList = Array.from({
|
|
37
|
+
length: pendingListLength,
|
|
38
|
+
}).map((_, i) => <PendingItem key={`pending_dialog_item_${i}`} />);
|
|
39
|
+
const handleOnScroll = async (offset) => {
|
|
40
|
+
props.onScroll?.(offset);
|
|
41
|
+
|
|
42
|
+
if (!vListRef.current) return;
|
|
43
|
+
|
|
44
|
+
if (typeof onScrollStartReached === 'function' && offset < offsetToReach) {
|
|
45
|
+
onScrollStartReached();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
typeof onScrollEndReached === 'function' &&
|
|
50
|
+
vListRef.current.viewportSize + offset + offsetToReach > vListRef.current.scrollSize
|
|
51
|
+
) {
|
|
52
|
+
onScrollEndReached();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useImperativeHandle(ref, () => vListRef.current || {}, []);
|
|
57
|
+
|
|
58
|
+
if (placeholderVisible) {
|
|
59
|
+
return <PlaceholderText titles={placeholderTitles} className="text-base text-muted" />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<VList ref={vListRef} onScroll={handleOnScroll} className="size-full overflow-y-scroll" {...props}>
|
|
64
|
+
{pending ? skeletonList : children}
|
|
65
|
+
</VList>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DialogsList = forwardRef(DialogsListBase);
|
|
70
|
+
|
|
71
|
+
DialogsList.displayName = 'DialogsList';
|
|
72
|
+
|
|
73
|
+
export { DialogsList };
|
|
@@ -73,7 +73,7 @@ function FilePickerInputBase(
|
|
|
73
73
|
{...labelProps}
|
|
74
74
|
htmlFor="file-uploader"
|
|
75
75
|
className={cn(
|
|
76
|
-
'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-
|
|
76
|
+
'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-out cursor-pointer',
|
|
77
77
|
labelProps?.className
|
|
78
78
|
)}
|
|
79
79
|
>
|
|
@@ -81,7 +81,7 @@ function FilePickerInputBase(
|
|
|
81
81
|
<Paperclip
|
|
82
82
|
{...iconProps}
|
|
83
83
|
className={cn(
|
|
84
|
-
'text-foreground group-hover:scale-110 transition-all duration-200 ease-
|
|
84
|
+
'text-foreground group-hover:scale-110 transition-all duration-200 ease-out',
|
|
85
85
|
iconProps?.className
|
|
86
86
|
)}
|
|
87
87
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, memo, useMemo } from 'react';
|
|
1
|
+
import { forwardRef, memo, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import Linkify from 'linkify-react';
|
|
3
3
|
import { cn } from './utils';
|
|
4
4
|
|
|
@@ -6,8 +6,22 @@ const DEFAULT_LINKIFY_OPTIONS = {
|
|
|
6
6
|
target: '_blank',
|
|
7
7
|
rel: 'noopener noreferrer',
|
|
8
8
|
};
|
|
9
|
+
const DEFAULT_SKELETON_LINES_CLASS_NAMES = ['w-full', 'w-3/4', 'w-1/2'];
|
|
9
10
|
|
|
10
|
-
function LinkifyTextBase(
|
|
11
|
+
function LinkifyTextBase(
|
|
12
|
+
{
|
|
13
|
+
text,
|
|
14
|
+
pending = false,
|
|
15
|
+
onReady = () => {},
|
|
16
|
+
linkifyProps,
|
|
17
|
+
skeletonContainerProps,
|
|
18
|
+
skeletonLineProps,
|
|
19
|
+
skeletonLinesClassNames = DEFAULT_SKELETON_LINES_CLASS_NAMES,
|
|
20
|
+
...props
|
|
21
|
+
},
|
|
22
|
+
ref
|
|
23
|
+
) {
|
|
24
|
+
const pendingRef = useRef(pending);
|
|
11
25
|
const options = useMemo(
|
|
12
26
|
() => ({
|
|
13
27
|
...DEFAULT_LINKIFY_OPTIONS,
|
|
@@ -17,6 +31,31 @@ function LinkifyTextBase({ text, linkifyProps, ...props }, ref) {
|
|
|
17
31
|
[linkifyProps]
|
|
18
32
|
);
|
|
19
33
|
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (pendingRef.current && !pending) {
|
|
36
|
+
onReady();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pendingRef.current = pending;
|
|
40
|
+
}, [pending, onReady]);
|
|
41
|
+
|
|
42
|
+
if (pending) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
{...skeletonContainerProps}
|
|
46
|
+
className={cn('flex items-start flex-col size-full', skeletonContainerProps?.className)}
|
|
47
|
+
>
|
|
48
|
+
{skeletonLinesClassNames.map((width, index) => (
|
|
49
|
+
<span
|
|
50
|
+
{...skeletonLineProps}
|
|
51
|
+
key={`${width}_${index}`}
|
|
52
|
+
className={cn('bg-muted-foreground animate-pulse rounded-md my-1 h-4', skeletonLineProps?.className, width)}
|
|
53
|
+
></span>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
20
59
|
return (
|
|
21
60
|
<p ref={ref} {...props} className={cn('wrap-break-word text-base', props?.className)}>
|
|
22
61
|
<Linkify options={options}>{text}</Linkify>
|
|
@@ -12,7 +12,11 @@ function PlaceholderTextBase({ title, titles = [], rowProps, ...props }, ref) {
|
|
|
12
12
|
className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', props?.className)}
|
|
13
13
|
>
|
|
14
14
|
{rows.map((row, index) => (
|
|
15
|
-
<div
|
|
15
|
+
<div
|
|
16
|
+
key={`placeholder-text-${index}`}
|
|
17
|
+
{...rowProps}
|
|
18
|
+
className={cn('text-center text-muted-foreground', rowProps?.className)}
|
|
19
|
+
>
|
|
16
20
|
{row}
|
|
17
21
|
</div>
|
|
18
22
|
))}
|