@banbox/chat 1.0.0
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/README.md +215 -0
- package/dist/index.cjs +3408 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +556 -0
- package/dist/index.d.ts +556 -0
- package/dist/index.js +3385 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -0
- package/src/adapter/types.ts +146 -0
- package/src/chat/ChatImagePreviewModal.tsx +194 -0
- package/src/chat/ChatRoot.tsx +67 -0
- package/src/chat/InboxPopup.tsx +312 -0
- package/src/chat/SinglePopup.tsx +240 -0
- package/src/contexts/ChatUIContext.tsx +30 -0
- package/src/contexts/ChatUIProvider.tsx +38 -0
- package/src/contexts/GalleryContext.tsx +40 -0
- package/src/contexts/GalleryProvider.tsx +89 -0
- package/src/hooks/useDisableBodyScroll.ts +16 -0
- package/src/icons/index.tsx +248 -0
- package/src/index.ts +56 -0
- package/src/lottie/typingdotanimation2.json +1 -0
- package/src/modals/chat/ChatConfirmModal.tsx +104 -0
- package/src/modals/chat/ChatTranslateSettingsModal.tsx +180 -0
- package/src/types/index.ts +163 -0
- package/src/ui/Button.tsx +83 -0
- package/src/ui/Portal.tsx +40 -0
- package/src/ui/Select.tsx +74 -0
- package/src/ui/chat/AttachmentPreviewStrip.tsx +166 -0
- package/src/ui/chat/ChatComposerBar.tsx +231 -0
- package/src/ui/chat/ChatFooter.tsx +442 -0
- package/src/ui/chat/ChatHeader.tsx +24 -0
- package/src/ui/chat/ChatIdentity.tsx +145 -0
- package/src/ui/chat/ChatInquiryBar.tsx +57 -0
- package/src/ui/chat/ChatListHeader.tsx +179 -0
- package/src/ui/chat/ChatMessageItem.tsx +214 -0
- package/src/ui/chat/ChatScroll.tsx +64 -0
- package/src/ui/chat/ChatSpinner.tsx +49 -0
- package/src/ui/chat/ChatThreadItem.tsx +140 -0
- package/src/ui/chat/MessageHoverActions.tsx +120 -0
- package/src/ui/chat/ReplyCard.tsx +217 -0
- package/src/ui/chat/TypingIndicator.tsx +93 -0
- package/src/ui/chat/drop-up/BusinessCardDropup.tsx +253 -0
- package/src/ui/chat/drop-up/EmojiDropup.tsx +132 -0
- package/src/ui/chat/message-items/ChatAddressCard.tsx +130 -0
- package/src/ui/chat/message-items/ChatBubbleAudio.tsx +209 -0
- package/src/ui/chat/message-items/ChatBubbleFiles.tsx +80 -0
- package/src/ui/chat/message-items/ChatBubbleImages.tsx +120 -0
- package/src/ui/chat/message-items/ChatBubbleText.tsx +16 -0
- package/src/ui/chat/message-items/ChatBusinessCard.tsx +95 -0
- package/src/ui/chat/scrollToMessage.ts +61 -0
- package/src/ui/chat/types.ts +37 -0
- package/src/utils/cn.ts +6 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FC } from "react";
|
|
4
|
+
import { FileIcon, ChatXIcon } from "../../icons";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
/* =======================
|
|
8
|
+
Helpers
|
|
9
|
+
======================= */
|
|
10
|
+
|
|
11
|
+
const extColor = (ext: string): string => {
|
|
12
|
+
const e = ext.toLowerCase();
|
|
13
|
+
|
|
14
|
+
if (e === "pdf") {
|
|
15
|
+
return "text-[#D93025]";
|
|
16
|
+
}
|
|
17
|
+
if (e === "ppt" || e === "pptx") {
|
|
18
|
+
return "text-[#E69138]";
|
|
19
|
+
}
|
|
20
|
+
if (e === "doc" || e === "docx") {
|
|
21
|
+
return "text-[#2B579A]";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return "text-[#6B7280]";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/* =======================
|
|
28
|
+
Types
|
|
29
|
+
======================= */
|
|
30
|
+
|
|
31
|
+
export type FilePreview = {
|
|
32
|
+
name: string;
|
|
33
|
+
sizeMB: number;
|
|
34
|
+
ext: string;
|
|
35
|
+
href: string;
|
|
36
|
+
downloadName?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type FilePreviewChipProps = {
|
|
40
|
+
name: string;
|
|
41
|
+
sizeMB: number;
|
|
42
|
+
ext: string;
|
|
43
|
+
onRemove: () => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ImageThumbProps = {
|
|
47
|
+
url: string;
|
|
48
|
+
onRemove: () => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type AttachmentPreviewStripProps = {
|
|
52
|
+
imgPreviews: string[];
|
|
53
|
+
filePreviews: FilePreview[];
|
|
54
|
+
onRemoveImage: (index: number) => void;
|
|
55
|
+
onRemoveFile: (index: number) => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/* =======================
|
|
59
|
+
Sub Components
|
|
60
|
+
======================= */
|
|
61
|
+
|
|
62
|
+
export const FilePreviewChip: FC<FilePreviewChipProps> = ({ name, sizeMB, ext, onRemove }) => {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={cn(
|
|
66
|
+
"mr-2 inline-flex h-[65px] max-w-[185px] items-center gap-3 whitespace-nowrap rounded-sm",
|
|
67
|
+
"border border-[#e1e1e1] bg-white px-3 py-2",
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
71
|
+
<div className="min-w-0">
|
|
72
|
+
<div className="flex items-center gap-1">
|
|
73
|
+
<FileIcon className={cn("h-[18px] w-[18px]", extColor(ext))} />
|
|
74
|
+
<div className="truncate text-[13px] font-normal text-black">{name}</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="mt-2 flex items-center gap-2 text-xs text-[#636363]">
|
|
78
|
+
<span>{sizeMB.toFixed(1)} MB</span>
|
|
79
|
+
<span className="h-3 w-px bg-[#e1e1e1]" />
|
|
80
|
+
<span className="uppercase">{ext}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={onRemove}
|
|
88
|
+
title="Remove"
|
|
89
|
+
aria-label="Remove file"
|
|
90
|
+
className={cn(
|
|
91
|
+
"grid h-8 w-8 place-items-center rounded-full bg-white text-[#3D3D3D]",
|
|
92
|
+
"shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5",
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<ChatXIcon className="h-[18px] w-[18px]" />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ImageThumb: FC<ImageThumbProps> = ({ url, onRemove }) => {
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className={cn(
|
|
105
|
+
"relative mr-2 inline-block h-[65px] w-[65px] rounded-sm",
|
|
106
|
+
"border border-[#e1e1e1] bg-[#F7F7F7]",
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<img src={url} alt="" className="h-full w-full rounded-sm object-cover" loading="lazy" />
|
|
110
|
+
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={onRemove}
|
|
114
|
+
aria-label="Remove image"
|
|
115
|
+
title="Remove image"
|
|
116
|
+
className={cn(
|
|
117
|
+
"absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 place-items-center",
|
|
118
|
+
"rounded-full bg-black/30 text-white",
|
|
119
|
+
"shadow-[0px_2px_4px_0px_#A5A3AE4D]",
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
<ChatXIcon className="h-4 w-4 text-white" />
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/* =======================
|
|
129
|
+
Component
|
|
130
|
+
======================= */
|
|
131
|
+
|
|
132
|
+
/** Single-row, horizontally scrollable preview strip */
|
|
133
|
+
const AttachmentPreviewStrip: FC<AttachmentPreviewStripProps> = ({
|
|
134
|
+
imgPreviews,
|
|
135
|
+
filePreviews,
|
|
136
|
+
onRemoveFile,
|
|
137
|
+
onRemoveImage,
|
|
138
|
+
}) => {
|
|
139
|
+
const has = imgPreviews.length > 0 || filePreviews.length > 0;
|
|
140
|
+
|
|
141
|
+
if (!has) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className={cn("mx-auto mb-2 max-w-[410px] overflow-x-auto custom-scroll")}>
|
|
147
|
+
<div className="flex items-start whitespace-nowrap">
|
|
148
|
+
{filePreviews.map((file, index) => (
|
|
149
|
+
<FilePreviewChip
|
|
150
|
+
key={`${file.name}-${index}`}
|
|
151
|
+
name={file.name}
|
|
152
|
+
sizeMB={file.sizeMB}
|
|
153
|
+
ext={file.ext}
|
|
154
|
+
onRemove={() => onRemoveFile(index)}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
|
|
158
|
+
{imgPreviews.map((url, index) => (
|
|
159
|
+
<ImageThumb key={`${url}-${index}`} url={url} onRemove={() => onRemoveImage(index)} />
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export default AttachmentPreviewStrip;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { ArrowSendAngleIcon, ArrowSendIcon, RecordMicIcon, ChatXIcon } from "../../icons";
|
|
6
|
+
import { cn } from "../../utils/cn";
|
|
7
|
+
|
|
8
|
+
/* =======================
|
|
9
|
+
Types
|
|
10
|
+
======================= */
|
|
11
|
+
|
|
12
|
+
type ChatComposerBarProps = {
|
|
13
|
+
recording: boolean;
|
|
14
|
+
seconds: number;
|
|
15
|
+
|
|
16
|
+
// typing
|
|
17
|
+
isTyping: boolean;
|
|
18
|
+
textRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
19
|
+
text: string;
|
|
20
|
+
onTextChange: (v: string) => void;
|
|
21
|
+
onAutoGrow: () => void;
|
|
22
|
+
|
|
23
|
+
// attachments
|
|
24
|
+
hasAttachments: boolean;
|
|
25
|
+
canSendArrow: boolean;
|
|
26
|
+
|
|
27
|
+
// actions
|
|
28
|
+
startRecording: () => void;
|
|
29
|
+
stopRecording: (send: boolean) => void;
|
|
30
|
+
sendText: () => void;
|
|
31
|
+
sendAttachments: () => void;
|
|
32
|
+
|
|
33
|
+
// util
|
|
34
|
+
fmtTime: (s: number) => string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/* =======================
|
|
38
|
+
Gradients
|
|
39
|
+
======================= */
|
|
40
|
+
|
|
41
|
+
const idleGradient =
|
|
42
|
+
"linear-gradient(90.85deg, rgba(51, 201, 212, 0.5) 0%, rgba(39, 83, 251, 0.5) 29.98%, rgba(39, 83, 251, 0.5) 49.97%, rgba(39, 83, 251, 0.5) 64.96%, rgba(235, 67, 255, 0.5) 99.94%)";
|
|
43
|
+
|
|
44
|
+
const activeGradient =
|
|
45
|
+
"linear-gradient(90.85deg, #33C9D4 0%, #2753FB 29.98%, #2753FB 49.97%, #2753FB 64.96%, #EB43FF 99.94%)";
|
|
46
|
+
|
|
47
|
+
/* =======================
|
|
48
|
+
Component
|
|
49
|
+
======================= */
|
|
50
|
+
|
|
51
|
+
const ChatComposerBar = ({
|
|
52
|
+
recording,
|
|
53
|
+
seconds,
|
|
54
|
+
isTyping,
|
|
55
|
+
textRef,
|
|
56
|
+
text,
|
|
57
|
+
onTextChange,
|
|
58
|
+
onAutoGrow,
|
|
59
|
+
hasAttachments,
|
|
60
|
+
canSendArrow,
|
|
61
|
+
startRecording,
|
|
62
|
+
stopRecording,
|
|
63
|
+
sendText,
|
|
64
|
+
sendAttachments,
|
|
65
|
+
fmtTime,
|
|
66
|
+
}: ChatComposerBarProps) => {
|
|
67
|
+
const composingRef = useRef(false);
|
|
68
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
69
|
+
|
|
70
|
+
const isActiveBorder = isFocused || recording;
|
|
71
|
+
|
|
72
|
+
if (!recording) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex w-full items-stretch gap-2">
|
|
75
|
+
<div
|
|
76
|
+
className="w-full rounded-sm p-px transition-[background] duration-200"
|
|
77
|
+
style={{
|
|
78
|
+
background: isActiveBorder ? activeGradient : idleGradient,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<div className="flex min-h-[50px] w-full items-center justify-between rounded-[3px] bg-white">
|
|
82
|
+
<div className="flex w-full items-center justify-between p-[3px]">
|
|
83
|
+
{!isTyping ? (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={startRecording}
|
|
87
|
+
className="grid h-[46px] w-[46px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
|
|
88
|
+
title="Record voice"
|
|
89
|
+
aria-label="Record voice"
|
|
90
|
+
>
|
|
91
|
+
<RecordMicIcon className="h-6 w-6" />
|
|
92
|
+
</button>
|
|
93
|
+
) : null}
|
|
94
|
+
|
|
95
|
+
<textarea
|
|
96
|
+
ref={textRef}
|
|
97
|
+
rows={1}
|
|
98
|
+
autoFocus
|
|
99
|
+
placeholder="Type a message"
|
|
100
|
+
className="custom-scroll-hidden max-h-[200px] flex-1 resize-none bg-transparent px-3 py-2 outline-none placeholder:text-[#777]"
|
|
101
|
+
value={text}
|
|
102
|
+
onChange={(e) => onTextChange(e.target.value)}
|
|
103
|
+
onInput={onAutoGrow}
|
|
104
|
+
onFocus={() => setIsFocused(true)}
|
|
105
|
+
onBlur={() => setIsFocused(false)}
|
|
106
|
+
onCompositionStart={() => {
|
|
107
|
+
composingRef.current = true;
|
|
108
|
+
}}
|
|
109
|
+
onCompositionEnd={() => {
|
|
110
|
+
composingRef.current = false;
|
|
111
|
+
onAutoGrow();
|
|
112
|
+
}}
|
|
113
|
+
onKeyDown={(e) => {
|
|
114
|
+
if (e.key === "Enter" && e.shiftKey) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (composingRef.current) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (e.key === "Enter") {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
if (text.length > 0) {
|
|
123
|
+
sendText();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{!canSendArrow ? (
|
|
131
|
+
<div className="grid h-full w-px place-items-center bg-[#E7E7E7]" />
|
|
132
|
+
) : null}
|
|
133
|
+
|
|
134
|
+
<div className="px-2">
|
|
135
|
+
{isTyping ? (
|
|
136
|
+
<div className="flex items-center">
|
|
137
|
+
<div className="h-10 w-px border-l border-[#CCCCCC]" />
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={sendText}
|
|
141
|
+
className={cn(
|
|
142
|
+
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full",
|
|
143
|
+
"text-[#ff5301] hover:bg-[#f8f8f8]",
|
|
144
|
+
)}
|
|
145
|
+
title={hasAttachments ? "Send attachments" : "Send"}
|
|
146
|
+
aria-label="Send"
|
|
147
|
+
>
|
|
148
|
+
<ArrowSendAngleIcon className="h-6 w-6" />
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
) : (
|
|
152
|
+
<div className="flex items-center">
|
|
153
|
+
<div className="h-10 w-px border-l border-[#CCCCCC]" />
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
onClick={sendAttachments}
|
|
157
|
+
disabled={!hasAttachments}
|
|
158
|
+
className={cn(
|
|
159
|
+
"ms-1 grid h-[40px] w-[40px] place-items-center rounded-full hover:bg-[#f8f8f8]",
|
|
160
|
+
hasAttachments ? "text-[#ff5301]" : "text-[#B9C3D4]",
|
|
161
|
+
)}
|
|
162
|
+
title={hasAttachments ? "Send attachments" : "Send"}
|
|
163
|
+
aria-label="Send"
|
|
164
|
+
>
|
|
165
|
+
<ArrowSendIcon className="h-6 w-6" />
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* =======================
|
|
177
|
+
Recording state UI
|
|
178
|
+
======================= */
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex w-full items-stretch gap-2">
|
|
182
|
+
<div className="w-full rounded-sm p-px" style={{ background: activeGradient }}>
|
|
183
|
+
<div className="flex h-[52px] w-full items-center justify-between rounded-[3px] bg-white">
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className="ms-[3px] grid h-[46px] w-[46px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
|
|
187
|
+
aria-label="Recording"
|
|
188
|
+
title="Recording"
|
|
189
|
+
>
|
|
190
|
+
<RecordMicIcon
|
|
191
|
+
className={cn(
|
|
192
|
+
"h-6 w-6",
|
|
193
|
+
seconds % 2 === 0
|
|
194
|
+
? "text-[#929292]"
|
|
195
|
+
: "text-[#ff5301]",
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
</button>
|
|
199
|
+
|
|
200
|
+
<div className="px-3 text-[13px]">{fmtTime(seconds)}</div>
|
|
201
|
+
|
|
202
|
+
<div className="ml-auto flex items-center gap-3 pr-2">
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={() => stopRecording(false)}
|
|
206
|
+
className="grid h-8 w-8 place-items-center rounded-full text-[#3D3D3D] hover:bg-black/5"
|
|
207
|
+
title="Discard"
|
|
208
|
+
aria-label="Discard recording"
|
|
209
|
+
>
|
|
210
|
+
<ChatXIcon className="h-5 w-5" />
|
|
211
|
+
</button>
|
|
212
|
+
|
|
213
|
+
<div className="h-6 w-px bg-[#E7E7E7]" />
|
|
214
|
+
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={() => stopRecording(true)}
|
|
218
|
+
className="grid h-10 w-[40px] place-items-center rounded-full text-[#ff5301]"
|
|
219
|
+
title={hasAttachments ? "Send attachments" : "Send"}
|
|
220
|
+
aria-label="Send"
|
|
221
|
+
>
|
|
222
|
+
<ArrowSendAngleIcon className="h-6 w-6" />
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export default ChatComposerBar;
|