@anker-in/campaign-ui 0.3.4 → 0.3.5
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/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageList.js +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +2 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/types.d.ts +2 -1
- package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
- package/dist/cjs/components/credits/creditsBanner/index.js +2 -2
- package/dist/cjs/components/credits/creditsBanner/index.js.map +2 -2
- package/dist/cjs/stories/LiveChatWidget.stories.js +2 -9
- package/dist/cjs/stories/LiveChatWidget.stories.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageList.js +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +2 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/types.d.ts +2 -1
- package/dist/esm/components/credits/creditsBanner/index.js +2 -2
- package/dist/esm/components/credits/creditsBanner/index.js.map +2 -2
- package/dist/esm/stories/LiveChatWidget.stories.js +1 -8
- package/dist/esm/stories/LiveChatWidget.stories.js.map +2 -2
- package/package.json +2 -2
- package/src/components/LiveChatWidget/LiveChatWidget.tsx +20 -0
- package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +1 -1
- package/src/components/LiveChatWidget/components/MessageList.tsx +39 -44
- package/src/components/LiveChatWidget/hooks/useChatState.ts +4 -3
- package/src/components/LiveChatWidget/types.ts +2 -1
- package/src/components/credits/creditsBanner/index.tsx +5 -5
- package/src/stories/LiveChatWidget.stories.tsx +7 -12
- package/src/styles/livechat.css +29 -0
|
@@ -183,6 +183,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
183
183
|
clearSession,
|
|
184
184
|
} = chatState
|
|
185
185
|
|
|
186
|
+
// 初始化加载状态(用户未配置欢迎语时,显示 loading 直到接口返回)
|
|
187
|
+
const [isInitializing, setIsInitializing] = React.useState(false)
|
|
188
|
+
|
|
186
189
|
// API 调用
|
|
187
190
|
const { sendMessageStream, createSession } = useChatAPI({
|
|
188
191
|
apiBaseUrl,
|
|
@@ -371,6 +374,11 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
371
374
|
const handleCreateNewSession = useCallback(async () => {
|
|
372
375
|
if (!userId) return
|
|
373
376
|
|
|
377
|
+
// 如果用户没有配置欢迎语,显示 loading 状态
|
|
378
|
+
if (!welcomeMessage) {
|
|
379
|
+
setIsInitializing(true)
|
|
380
|
+
}
|
|
381
|
+
|
|
374
382
|
try {
|
|
375
383
|
const response = await createSession({
|
|
376
384
|
user_id: userId,
|
|
@@ -450,6 +458,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
450
458
|
],
|
|
451
459
|
timestamp: Date.now(),
|
|
452
460
|
})
|
|
461
|
+
} finally {
|
|
462
|
+
// 接口返回后关闭 loading 状态
|
|
463
|
+
setIsInitializing(false)
|
|
453
464
|
}
|
|
454
465
|
}, [
|
|
455
466
|
userId,
|
|
@@ -470,6 +481,11 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
470
481
|
*/
|
|
471
482
|
const handleResumeSession = useCallback(
|
|
472
483
|
async (existingSessionId: string) => {
|
|
484
|
+
// 如果用户没有配置欢迎语,显示 loading 状态
|
|
485
|
+
if (!welcomeMessage) {
|
|
486
|
+
setIsInitializing(true)
|
|
487
|
+
}
|
|
488
|
+
|
|
473
489
|
try {
|
|
474
490
|
const response = await createSession({
|
|
475
491
|
user_id: userId,
|
|
@@ -590,6 +606,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
590
606
|
clearSession()
|
|
591
607
|
handleCreateNewSession()
|
|
592
608
|
}
|
|
609
|
+
} finally {
|
|
610
|
+
// 接口返回后关闭 loading 状态
|
|
611
|
+
setIsInitializing(false)
|
|
593
612
|
}
|
|
594
613
|
},
|
|
595
614
|
[
|
|
@@ -873,6 +892,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
873
892
|
title={title}
|
|
874
893
|
logoUrl={logoUrl}
|
|
875
894
|
isSending={isStreaming}
|
|
895
|
+
isLoadingHistory={isInitializing}
|
|
876
896
|
rendererRegistry={rendererRegistry}
|
|
877
897
|
inputPlaceholder=""
|
|
878
898
|
onAddToCart={onAddToCart}
|
|
@@ -103,7 +103,7 @@ export const PromotionList: React.FC<PromotionListProps> = ({ data, isUser = fal
|
|
|
103
103
|
return (
|
|
104
104
|
<div className="space-y-3">
|
|
105
105
|
{results.map(promotion => {
|
|
106
|
-
const bannerUrl = promotion.banner_url ||promotion?.metadata?.banner_url
|
|
106
|
+
const bannerUrl = promotion.banner_url || promotion?.metadata?.banner_url
|
|
107
107
|
|
|
108
108
|
// 没有图片则不展示
|
|
109
109
|
if (!bannerUrl) {
|
|
@@ -117,33 +117,37 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
117
117
|
className = '',
|
|
118
118
|
onAddToCart,
|
|
119
119
|
}) => {
|
|
120
|
-
|
|
120
|
+
// 使用 callback ref + state 确保 DOM 挂载后触发重新渲染
|
|
121
|
+
const [listElement, setListElement] = useState<HTMLDivElement | null>(null)
|
|
122
|
+
const listRef = useCallback((node: HTMLDivElement | null) => {
|
|
123
|
+
setListElement(node)
|
|
124
|
+
}, [])
|
|
121
125
|
const [showScrollButton, setShowScrollButton] = useState(false)
|
|
122
126
|
|
|
123
127
|
// 检查是否接近底部
|
|
124
|
-
const isNearBottom = useCallback(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
const isNearBottom = useCallback(
|
|
129
|
+
(threshold = 100) => {
|
|
130
|
+
if (!listElement) return true
|
|
131
|
+
|
|
132
|
+
const { scrollTop, scrollHeight, clientHeight } = listElement
|
|
133
|
+
return scrollHeight - scrollTop - clientHeight < threshold
|
|
134
|
+
},
|
|
135
|
+
[listElement]
|
|
136
|
+
)
|
|
131
137
|
|
|
132
138
|
// 平滑滚动到底部
|
|
133
139
|
const scrollToBottom = useCallback(() => {
|
|
134
|
-
|
|
135
|
-
if (!container) return
|
|
140
|
+
if (!listElement) return
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
top:
|
|
142
|
+
listElement.scrollTo({
|
|
143
|
+
top: listElement.scrollHeight,
|
|
139
144
|
behavior: 'smooth',
|
|
140
145
|
})
|
|
141
|
-
}, [])
|
|
146
|
+
}, [listElement])
|
|
142
147
|
|
|
143
148
|
// 监听滚动事件,控制按钮显示
|
|
144
149
|
useEffect(() => {
|
|
145
|
-
|
|
146
|
-
if (!container) return
|
|
150
|
+
if (!listElement) return
|
|
147
151
|
|
|
148
152
|
const handleScroll = () => {
|
|
149
153
|
setShowScrollButton(!isNearBottom())
|
|
@@ -152,9 +156,9 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
152
156
|
// 初始检查
|
|
153
157
|
handleScroll()
|
|
154
158
|
|
|
155
|
-
|
|
156
|
-
return () =>
|
|
157
|
-
}, [isNearBottom])
|
|
159
|
+
listElement.addEventListener('scroll', handleScroll, { passive: true })
|
|
160
|
+
return () => listElement.removeEventListener('scroll', handleScroll)
|
|
161
|
+
}, [listElement, isNearBottom])
|
|
158
162
|
|
|
159
163
|
// 当消息列表变化时,重新检查按钮状态(但不在自动滚动时)
|
|
160
164
|
useEffect(() => {
|
|
@@ -169,22 +173,31 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
169
173
|
|
|
170
174
|
// 监听消息列表变化,自动滚动
|
|
171
175
|
useEffect(() => {
|
|
172
|
-
if (!autoScroll || !
|
|
176
|
+
if (!autoScroll || !listElement) return
|
|
173
177
|
|
|
174
178
|
// 延迟滚动以确保 DOM 已更新
|
|
175
179
|
const timeoutId = setTimeout(() => {
|
|
176
|
-
if (
|
|
177
|
-
|
|
180
|
+
if (listElement) {
|
|
181
|
+
listElement.scrollTop = listElement.scrollHeight
|
|
178
182
|
// 自动滚动到底部后,隐藏按钮
|
|
179
183
|
setShowScrollButton(false)
|
|
180
184
|
}
|
|
181
185
|
}, 100)
|
|
182
186
|
|
|
183
187
|
return () => clearTimeout(timeoutId)
|
|
184
|
-
}, [messages, autoScroll])
|
|
185
|
-
|
|
186
|
-
// 空状态
|
|
187
|
-
if (messages.length === 0
|
|
188
|
+
}, [messages, autoScroll, listElement])
|
|
189
|
+
|
|
190
|
+
// 空状态 + 加载中
|
|
191
|
+
if (messages.length === 0) {
|
|
192
|
+
if (isLoadingHistory) {
|
|
193
|
+
// 加载中,显示 loading
|
|
194
|
+
return (
|
|
195
|
+
<div className={`flex flex-1 items-center justify-center overflow-hidden ${className}`}>
|
|
196
|
+
<LoadingIndicator />
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
// 空状态
|
|
188
201
|
return (
|
|
189
202
|
<div className={`flex-1 overflow-hidden ${className}`}>{emptyPlaceholder || <DefaultEmptyPlaceholder />}</div>
|
|
190
203
|
)
|
|
@@ -199,9 +212,6 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
199
212
|
${className}
|
|
200
213
|
`}
|
|
201
214
|
>
|
|
202
|
-
{/* 加载历史消息指示器 */}
|
|
203
|
-
{isLoadingHistory && <LoadingIndicator />}
|
|
204
|
-
|
|
205
215
|
{/* 消息列表 */}
|
|
206
216
|
<div className="flex flex-col gap-1">
|
|
207
217
|
{messages.map(message => (
|
|
@@ -223,22 +233,7 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
223
233
|
{/* 回到底部按钮 */}
|
|
224
234
|
<button
|
|
225
235
|
onClick={scrollToBottom}
|
|
226
|
-
className={`
|
|
227
|
-
style={{
|
|
228
|
-
position: 'absolute',
|
|
229
|
-
bottom: '24px',
|
|
230
|
-
left: '50%',
|
|
231
|
-
zIndex: 10,
|
|
232
|
-
width: '40px',
|
|
233
|
-
height: '40px',
|
|
234
|
-
borderRadius: '50%',
|
|
235
|
-
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
236
|
-
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
237
|
-
transition: 'all 300ms ease-in-out',
|
|
238
|
-
opacity: showScrollButton ? 1 : 0,
|
|
239
|
-
cursor: 'pointer',
|
|
240
|
-
border: 'none',
|
|
241
|
-
}}
|
|
236
|
+
className={`livechat-scroll-to-bottom ${showScrollButton ? 'visible' : 'hidden'}`}
|
|
242
237
|
aria-label="Scroll to bottom"
|
|
243
238
|
aria-hidden={!showScrollButton}
|
|
244
239
|
>
|
|
@@ -357,8 +357,9 @@ export interface UseChatStateOptions {
|
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
359
|
* AI 回复促销卡片时触发
|
|
360
|
+
* @param promotions 促销活动数组数据
|
|
360
361
|
*/
|
|
361
|
-
onPromotionList?: () => void
|
|
362
|
+
onPromotionList?: (promotions: any[]) => void
|
|
362
363
|
|
|
363
364
|
/**
|
|
364
365
|
* 商品添加到购物车回调
|
|
@@ -852,8 +853,8 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
|
|
|
852
853
|
// ========== 6. 促销活动列表 (Promotion List) ==========
|
|
853
854
|
// 后端格式: {type: "promotion_list", data: {found, count, total, results: [...]}}
|
|
854
855
|
else if (contentType === 'promotion_list' && contentData.found !== undefined) {
|
|
855
|
-
//
|
|
856
|
-
onPromotionList?.()
|
|
856
|
+
// 触发促销卡片回调,传递促销活动数组
|
|
857
|
+
onPromotionList?.(contentData.results || [])
|
|
857
858
|
|
|
858
859
|
messageContent = {
|
|
859
860
|
type: 'promotion_list',
|
|
@@ -80,25 +80,25 @@ export function CreditsBanner({ copy, id }: { copy: CreditsBannerCopy; id?: stri
|
|
|
80
80
|
></div>
|
|
81
81
|
)}
|
|
82
82
|
|
|
83
|
-
<Container className="l:h-auto !absolute inset-0 mx-auto grid h-full" asChild>
|
|
83
|
+
<Container className="l-tablet:h-auto !absolute inset-0 mx-auto grid h-full" asChild>
|
|
84
84
|
<div className="grid grid-cols-12">
|
|
85
|
-
<div className="l:col-span-12 l:justify-start l:truncate l:pt-[32px] col-span-5 flex h-full flex-col justify-center text-[#1F2021]">
|
|
85
|
+
<div className="l-tablet:col-span-12 l-tablet:justify-start l-tablet:truncate l-tablet:pt-[32px] col-span-5 flex h-full flex-col justify-center text-[#1F2021]">
|
|
86
86
|
<Heading
|
|
87
87
|
as="h1"
|
|
88
|
-
className="text-[48px] font-bold xl-xxl:text-[40px] l:text-[32px]"
|
|
88
|
+
className="text-[48px] font-bold xl-xxl:text-[40px] l-tablet:text-[32px]"
|
|
89
89
|
html={isLogin ? copy.login.title?.replace('$name', displayName || '') : copy.unLogin.title}
|
|
90
90
|
></Heading>
|
|
91
91
|
|
|
92
92
|
<Text
|
|
93
93
|
size="3"
|
|
94
|
-
className="l:mt-[4px] l-xxl:text-[14px] mt-[16px]"
|
|
94
|
+
className="l-tablet:mt-[4px] l-xxl:text-[14px] mt-[16px]"
|
|
95
95
|
html={isLogin ? copy.login.description : copy.unLogin.description}
|
|
96
96
|
></Text>
|
|
97
97
|
|
|
98
98
|
{!isLogin && (
|
|
99
99
|
<div
|
|
100
100
|
className={classNames(
|
|
101
|
-
'mt-[32px] grid w-fit grid-flow-col gap-[12px] l:mt-[24px]',
|
|
101
|
+
'mt-[32px] grid w-fit grid-flow-col gap-[12px] l-tablet:mt-[24px]',
|
|
102
102
|
isLogin && 'hidden'
|
|
103
103
|
)}
|
|
104
104
|
>
|
|
@@ -147,7 +147,7 @@ type Story = StoryObj<typeof LiveChatWidget>
|
|
|
147
147
|
export const Default: Story = {
|
|
148
148
|
args: {
|
|
149
149
|
// 基础配置
|
|
150
|
-
loginUserId: '
|
|
150
|
+
loginUserId: 'test_test1',
|
|
151
151
|
apiBaseUrl: 'http://172.16.38.183:3003',
|
|
152
152
|
site: 'beta.eufy.com',
|
|
153
153
|
channelCode: 'dtc',
|
|
@@ -159,14 +159,7 @@ export const Default: Story = {
|
|
|
159
159
|
position: { bottom: '24px', right: '30px' },
|
|
160
160
|
|
|
161
161
|
// 欢迎消息
|
|
162
|
-
welcomeMessage:
|
|
163
|
-
|
|
164
|
-
I can help you with:
|
|
165
|
-
- Product recommendations
|
|
166
|
-
- Order tracking
|
|
167
|
-
- FAQs and support
|
|
168
|
-
|
|
169
|
-
How can I assist you today?`,
|
|
162
|
+
welcomeMessage: '',
|
|
170
163
|
|
|
171
164
|
// 快捷回复
|
|
172
165
|
quickReplies: [
|
|
@@ -179,9 +172,11 @@ How can I assist you today?`,
|
|
|
179
172
|
// 法规协议弹窗
|
|
180
173
|
complianceConfig: {
|
|
181
174
|
title: "Hi! I'm your eufy AI assistant.",
|
|
182
|
-
content:
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
content:
|
|
176
|
+
'AI-generated responses can be inaccurate. Please verify important info. Do not input sensitive personal data.',
|
|
177
|
+
checkboxText:
|
|
178
|
+
'By starting to use "Live Chat", you agree to Anker\'s <a href="https://www.anker.com/pages/privacy-policy" target="_blank" rel="noopener noreferrer" style="text-decoration: underline;">LIVE CHAT PRIVACY NOTICE</a>.',
|
|
179
|
+
agreeButtonText: 'Agree',
|
|
185
180
|
},
|
|
186
181
|
|
|
187
182
|
// reCAPTCHA 配置(取消注释以启用)
|
package/src/styles/livechat.css
CHANGED
|
@@ -144,6 +144,35 @@
|
|
|
144
144
|
background: var(--livechat-text-secondary);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/* 回到底部按钮样式 */
|
|
148
|
+
.livechat-scroll-to-bottom {
|
|
149
|
+
position: absolute;
|
|
150
|
+
bottom: 24px;
|
|
151
|
+
left: 50%;
|
|
152
|
+
transform: translateX(-50%);
|
|
153
|
+
z-index: 10;
|
|
154
|
+
width: 40px;
|
|
155
|
+
height: 40px;
|
|
156
|
+
border-radius: 50%;
|
|
157
|
+
background-color: rgba(255, 255, 255, 0.95);
|
|
158
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
159
|
+
transition: opacity 300ms ease-in-out;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
border: none;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.livechat-scroll-to-bottom.hidden {
|
|
168
|
+
opacity: 0 !important;
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.livechat-scroll-to-bottom.visible {
|
|
173
|
+
opacity: 1 !important;
|
|
174
|
+
}
|
|
175
|
+
|
|
147
176
|
/* 思考状态动画 */
|
|
148
177
|
.livechat-thinking-dots {
|
|
149
178
|
display: inline-flex;
|