@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.
Files changed (36) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  3. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +2 -2
  4. package/dist/cjs/components/LiveChatWidget/components/MessageList.js +3 -3
  5. package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +3 -3
  6. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +2 -1
  7. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  8. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +2 -2
  9. package/dist/cjs/components/LiveChatWidget/types.d.ts +2 -1
  10. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  11. package/dist/cjs/components/credits/creditsBanner/index.js +2 -2
  12. package/dist/cjs/components/credits/creditsBanner/index.js.map +2 -2
  13. package/dist/cjs/stories/LiveChatWidget.stories.js +2 -9
  14. package/dist/cjs/stories/LiveChatWidget.stories.js.map +2 -2
  15. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  16. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  17. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +2 -2
  18. package/dist/esm/components/LiveChatWidget/components/MessageList.js +3 -3
  19. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
  20. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +2 -1
  21. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  22. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +2 -2
  23. package/dist/esm/components/LiveChatWidget/types.d.ts +2 -1
  24. package/dist/esm/components/credits/creditsBanner/index.js +2 -2
  25. package/dist/esm/components/credits/creditsBanner/index.js.map +2 -2
  26. package/dist/esm/stories/LiveChatWidget.stories.js +1 -8
  27. package/dist/esm/stories/LiveChatWidget.stories.js.map +2 -2
  28. package/package.json +2 -2
  29. package/src/components/LiveChatWidget/LiveChatWidget.tsx +20 -0
  30. package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +1 -1
  31. package/src/components/LiveChatWidget/components/MessageList.tsx +39 -44
  32. package/src/components/LiveChatWidget/hooks/useChatState.ts +4 -3
  33. package/src/components/LiveChatWidget/types.ts +2 -1
  34. package/src/components/credits/creditsBanner/index.tsx +5 -5
  35. package/src/stories/LiveChatWidget.stories.tsx +7 -12
  36. 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
- const listRef = useRef<HTMLDivElement>(null)
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((threshold = 100) => {
125
- const container = listRef.current
126
- if (!container) return true
127
-
128
- const { scrollTop, scrollHeight, clientHeight } = container
129
- return scrollHeight - scrollTop - clientHeight < threshold
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
- const container = listRef.current
135
- if (!container) return
140
+ if (!listElement) return
136
141
 
137
- container.scrollTo({
138
- top: container.scrollHeight,
142
+ listElement.scrollTo({
143
+ top: listElement.scrollHeight,
139
144
  behavior: 'smooth',
140
145
  })
141
- }, [])
146
+ }, [listElement])
142
147
 
143
148
  // 监听滚动事件,控制按钮显示
144
149
  useEffect(() => {
145
- const container = listRef.current
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
- container.addEventListener('scroll', handleScroll, { passive: true })
156
- return () => container.removeEventListener('scroll', handleScroll)
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 || !listRef.current) return
176
+ if (!autoScroll || !listElement) return
173
177
 
174
178
  // 延迟滚动以确保 DOM 已更新
175
179
  const timeoutId = setTimeout(() => {
176
- if (listRef.current) {
177
- listRef.current.scrollTop = listRef.current.scrollHeight
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 && !isLoadingHistory) {
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={`flex -translate-x-1/2 items-center justify-center ${showScrollButton ? '' : 'pointer-events-none'}`}
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',
@@ -923,8 +923,9 @@ export interface LiveChatWidgetProps {
923
923
 
924
924
  /**
925
925
  * AI 回复促销卡片时触发
926
+ * @param promotions 促销活动数组数据
926
927
  */
927
- onPromotionList?: () => void
928
+ onPromotionList?: (promotions: PromotionItem[]) => void
928
929
 
929
930
  /**
930
931
  * 商品操作回调
@@ -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: 'test_test',
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: `Welcome to eufy AI Assistant!
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: "AI-generated responses can be inaccurate. Please verify important info. Do not input sensitive personal data.",
183
- checkboxText: '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>.',
184
- agreeButtonText: "Agree"
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 配置(取消注释以启用)
@@ -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;