@anker-in/campaign-ui 0.3.4 → 0.3.6

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 (56) 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 +8 -3
  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/index.d.ts +1 -1
  10. package/dist/cjs/components/LiveChatWidget/index.js +1 -1
  11. package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
  12. package/dist/cjs/components/LiveChatWidget/types.d.ts +4 -1
  13. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  14. package/dist/cjs/components/LiveChatWidget/utils/userId.d.ts +7 -2
  15. package/dist/cjs/components/LiveChatWidget/utils/userId.js +1 -1
  16. package/dist/cjs/components/LiveChatWidget/utils/userId.js.map +3 -3
  17. package/dist/cjs/components/credits/creditsBanner/index.js +2 -2
  18. package/dist/cjs/components/credits/creditsBanner/index.js.map +2 -2
  19. package/dist/cjs/stories/LiveChatWidget.stories.js +2 -9
  20. package/dist/cjs/stories/LiveChatWidget.stories.js.map +2 -2
  21. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  22. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  23. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +2 -2
  24. package/dist/esm/components/LiveChatWidget/components/MessageList.js +3 -3
  25. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
  26. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +8 -3
  27. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  28. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  29. package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
  30. package/dist/esm/components/LiveChatWidget/index.js +1 -1
  31. package/dist/esm/components/LiveChatWidget/index.js.map +3 -3
  32. package/dist/esm/components/LiveChatWidget/types.d.ts +4 -1
  33. package/dist/esm/components/LiveChatWidget/utils/userId.d.ts +7 -2
  34. package/dist/esm/components/LiveChatWidget/utils/userId.js +1 -1
  35. package/dist/esm/components/LiveChatWidget/utils/userId.js.map +3 -3
  36. package/dist/esm/components/credits/creditsBanner/index.js +2 -2
  37. package/dist/esm/components/credits/creditsBanner/index.js.map +2 -2
  38. package/dist/esm/stories/LiveChatWidget.stories.js +1 -8
  39. package/dist/esm/stories/LiveChatWidget.stories.js.map +2 -2
  40. package/dist/index.d.mts +140 -13
  41. package/dist/index.d.ts +140 -13
  42. package/dist/index.js +2109 -6424
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +1901 -6216
  45. package/dist/index.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/components/LiveChatWidget/LiveChatWidget.tsx +60 -7
  48. package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +1 -1
  49. package/src/components/LiveChatWidget/components/MessageList.tsx +39 -44
  50. package/src/components/LiveChatWidget/hooks/useChatState.ts +22 -10
  51. package/src/components/LiveChatWidget/index.tsx +1 -1
  52. package/src/components/LiveChatWidget/types.ts +4 -1
  53. package/src/components/LiveChatWidget/utils/userId.ts +13 -62
  54. package/src/components/credits/creditsBanner/index.tsx +5 -5
  55. package/src/stories/LiveChatWidget.stories.tsx +8 -13
  56. package/src/styles/livechat.css +29 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anker-in/campaign-ui",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Campaign UI components and utilities for Anker projects",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -95,7 +95,7 @@
95
95
  "swiper": "^11.1.3",
96
96
  "tailwind-merge": "^2.3.0",
97
97
  "tailwindcss": "^3.4.3",
98
- "@anker-in/lib": "1.1.2-beta.2"
98
+ "@anker-in/lib": "1.1.3"
99
99
  },
100
100
  "publishConfig": {
101
101
  "access": "public",
@@ -23,6 +23,7 @@ import { useChatState } from './hooks/useChatState'
23
23
  import { useChatAPI } from './hooks/useChatAPI'
24
24
  import { MessageRendererRegistry } from './utils/messageRenderers'
25
25
  import { sanitizeInput } from './utils/validation'
26
+ import { saveUserId } from './utils/userId'
26
27
  import { transformProducts } from './utils/productTransformers.js'
27
28
  import { transformCartData } from './utils/cartTransformers.js'
28
29
  import Cookies from 'js-cookie'
@@ -175,6 +176,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
175
176
  openChat,
176
177
  closeChat,
177
178
  setInputValue,
179
+ setUserId,
178
180
  addMessage,
179
181
  setMessages,
180
182
  clearMessages,
@@ -183,6 +185,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
183
185
  clearSession,
184
186
  } = chatState
185
187
 
188
+ // 初始化加载状态(用户未配置欢迎语时,显示 loading 直到接口返回)
189
+ const [isInitializing, setIsInitializing] = React.useState(false)
190
+
186
191
  // API 调用
187
192
  const { sendMessageStream, createSession } = useChatAPI({
188
193
  apiBaseUrl,
@@ -229,6 +234,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
229
234
  return registry
230
235
  }, [customRenderers])
231
236
 
237
+ // 标记是否已经初始化过会话,防止重复调用
238
+ const sessionInitializedRef = React.useRef(false)
239
+
232
240
  /**
233
241
  * T043: 打开聊天窗口时初始化会话
234
242
  * 使用 API v2.0.0 的统一接口:
@@ -236,7 +244,18 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
236
244
  * - 如果有 sessionId,恢复会话并加载历史消息
237
245
  */
238
246
  useEffect(() => {
239
- if (!isOpen || !userId) return
247
+ // 窗口关闭时重置初始化标记
248
+ if (!isOpen) {
249
+ sessionInitializedRef.current = false
250
+ return
251
+ }
252
+
253
+ // userId 为 undefined 表示尚未初始化,等待初始化完成
254
+ if (userId === undefined) return
255
+
256
+ // 如果已经初始化过,不再重复调用
257
+ if (sessionInitializedRef.current) return
258
+ sessionInitializedRef.current = true
240
259
 
241
260
  const currentSessionId = sessionId
242
261
 
@@ -369,20 +388,33 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
369
388
  * 创建新会话
370
389
  */
371
390
  const handleCreateNewSession = useCallback(async () => {
372
- if (!userId) return
391
+ // userId 为 undefined 表示尚未初始化,等待初始化完成
392
+ if (userId === undefined) return
393
+
394
+ // 如果用户没有配置欢迎语,显示 loading 状态
395
+ if (!welcomeMessage) {
396
+ setIsInitializing(true)
397
+ }
373
398
 
374
399
  try {
375
400
  const response = await createSession({
376
- user_id: userId,
401
+ user_id: userId ?? '',
377
402
  site: site,
378
403
  channel_code: channelCode,
379
404
  real_user_id: loginUserId,
405
+ page_url: typeof window !== 'undefined' ? window.location.href : undefined,
380
406
  })
381
407
 
382
408
  if (response.success) {
383
409
  // 保存新会话 ID
384
410
  saveSession(response.sessionId)
385
411
 
412
+ // 保存后端返回的 userId(如果有)
413
+ if (response.userId) {
414
+ saveUserId(response.userId)
415
+ setUserId(response.userId)
416
+ }
417
+
386
418
  // 清空消息列表
387
419
  clearMessages()
388
420
 
@@ -450,6 +482,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
450
482
  ],
451
483
  timestamp: Date.now(),
452
484
  })
485
+ } finally {
486
+ // 接口返回后关闭 loading 状态
487
+ setIsInitializing(false)
453
488
  }
454
489
  }, [
455
490
  userId,
@@ -470,18 +505,30 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
470
505
  */
471
506
  const handleResumeSession = useCallback(
472
507
  async (existingSessionId: string) => {
508
+ // 如果用户没有配置欢迎语,显示 loading 状态
509
+ if (!welcomeMessage) {
510
+ setIsInitializing(true)
511
+ }
512
+
473
513
  try {
474
514
  const response = await createSession({
475
- user_id: userId,
515
+ user_id: userId ?? '',
476
516
  session_id: existingSessionId,
477
517
  site: site,
478
518
  channel_code: channelCode,
479
519
  real_user_id: loginUserId,
520
+ page_url: typeof window !== 'undefined' ? window.location.href : undefined,
480
521
  })
481
522
 
482
523
  if (response.success && response.resumed) {
483
524
  // 会话恢复成功
484
525
 
526
+ // 保存后端返回的 userId(如果有)
527
+ if (response.userId) {
528
+ saveUserId(response.userId)
529
+ setUserId(response.userId)
530
+ }
531
+
485
532
  // 准备欢迎消息(无论是否有历史消息都需要)
486
533
  const messageText = response.welcomeMessage || welcomeMessage
487
534
  const welcomeContent: MessageContent[] = messageText ? [{ type: 'text', text: messageText }] : []
@@ -590,6 +637,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
590
637
  clearSession()
591
638
  handleCreateNewSession()
592
639
  }
640
+ } finally {
641
+ // 接口返回后关闭 loading 状态
642
+ setIsInitializing(false)
593
643
  }
594
644
  },
595
645
  [
@@ -664,10 +714,11 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
664
714
  if (!currentSessionId) {
665
715
  // 没有会话,创建新会话
666
716
  const response = await createSession({
667
- user_id: userId,
717
+ user_id: userId ?? '',
668
718
  site: site,
669
719
  channel_code: channelCode,
670
720
  real_user_id: loginUserId,
721
+ page_url: typeof window !== 'undefined' ? window.location.href : undefined,
671
722
  })
672
723
  if (response.success) {
673
724
  currentSessionId = response.sessionId
@@ -680,7 +731,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
680
731
  // 构建请求参数(session_id 现在是必填的)
681
732
  const requestPayload: ChatStreamRequest = {
682
733
  message: sanitized,
683
- user_id: userId,
734
+ user_id: userId ?? '',
684
735
  session_id: currentSessionId,
685
736
  context: {
686
737
  cartId: cartId,
@@ -711,10 +762,11 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
711
762
 
712
763
  // 创建新会话
713
764
  const response = await createSession({
714
- user_id: userId,
765
+ user_id: userId ?? '',
715
766
  site: site,
716
767
  channel_code: channelCode,
717
768
  real_user_id: loginUserId,
769
+ page_url: typeof window !== 'undefined' ? window.location.href : undefined,
718
770
  })
719
771
 
720
772
  if (response.success) {
@@ -873,6 +925,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
873
925
  title={title}
874
926
  logoUrl={logoUrl}
875
927
  isSending={isStreaming}
928
+ isLoadingHistory={isInitializing}
876
929
  rendererRegistry={rendererRegistry}
877
930
  inputPlaceholder=""
878
931
  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
  >
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { useState, useCallback, useRef, useEffect } from 'react'
8
8
  import type { Message, MessageContent, SSEEvent, StatusData, ErrorData, MessageStartData, BackendCartData, Product, TextContent, ProductCardContent, ProductListContent } from '../types'
9
- import { getUserId } from '../utils/userId'
9
+ import { getUserId, saveUserId } from '../utils/userId'
10
10
  import { useSession } from './useSession'
11
11
  import { transformProducts } from '../utils/productTransformers'
12
12
  import { transformCartData } from '../utils/cartTransformers'
@@ -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
  * 商品添加到购物车回调
@@ -390,9 +391,9 @@ export interface UseChatStateReturn {
390
391
  isOpen: boolean
391
392
 
392
393
  /**
393
- * 用户 ID
394
+ * 用户 ID(undefined 表示尚未初始化,空字符串表示由后端生成)
394
395
  */
395
- userId: string
396
+ userId: string | undefined
396
397
 
397
398
  /**
398
399
  * 会话 ID
@@ -429,6 +430,11 @@ export interface UseChatStateReturn {
429
430
  */
430
431
  setInputValue: (value: string) => void
431
432
 
433
+ /**
434
+ * 设置用户 ID(用于保存后端返回的 userId)
435
+ */
436
+ setUserId: (id: string) => void
437
+
432
438
  /**
433
439
  * 添加消息到列表
434
440
  */
@@ -494,8 +500,8 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
494
500
  // 会话管理
495
501
  const { sessionId, saveSession, clearSession } = useSession()
496
502
 
497
- // 用户 ID (初始化时异步生成)
498
- const [userId, setUserId] = useState<string>('')
503
+ // 用户 ID (初始化时异步生成,undefined 表示尚未初始化)
504
+ const [userId, setUserId] = useState<string | undefined>(undefined)
499
505
 
500
506
  // 初始化 userId
501
507
  useEffect(() => {
@@ -645,11 +651,16 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
645
651
  // 重置卡片缓存队列
646
652
  pendingCardsRef.current = []
647
653
 
648
- // T039: 保存 sessionId(如果后端返回)
649
- const messageStartData = data as MessageStartData
654
+ // T039: 保存 sessionId 和 userId(如果后端返回)
655
+ const messageStartData = data as MessageStartData & { userId?: string }
650
656
  if (messageStartData.sessionId && messageStartData.sessionId !== sessionId) {
651
657
  saveSession(messageStartData.sessionId)
652
658
  }
659
+ // 保存后端返回的 userId(如果有)
660
+ if (messageStartData.userId) {
661
+ saveUserId(messageStartData.userId)
662
+ setUserId(messageStartData.userId)
663
+ }
653
664
 
654
665
  // 检查最后一条消息是否是 thinking 消息(用户发送消息时已添加)
655
666
  setMessagesState(prev => {
@@ -852,8 +863,8 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
852
863
  // ========== 6. 促销活动列表 (Promotion List) ==========
853
864
  // 后端格式: {type: "promotion_list", data: {found, count, total, results: [...]}}
854
865
  else if (contentType === 'promotion_list' && contentData.found !== undefined) {
855
- // 触发促销卡片回调
856
- onPromotionList?.()
866
+ // 触发促销卡片回调,传递促销活动数组
867
+ onPromotionList?.(contentData.results || [])
857
868
 
858
869
  messageContent = {
859
870
  type: 'promotion_list',
@@ -1080,6 +1091,7 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
1080
1091
  closeChat,
1081
1092
  toggleChat,
1082
1093
  setInputValue,
1094
+ setUserId,
1083
1095
  addMessage,
1084
1096
  setMessages,
1085
1097
  clearMessages,
@@ -44,7 +44,7 @@ export { useSession } from './hooks/useSession'
44
44
 
45
45
  // 导出工具类
46
46
  export { MessageRendererRegistry } from './utils/messageRenderers'
47
- export { getUserId } from './utils/userId'
47
+ export { getUserId, saveUserId, clearUserId } from './utils/userId'
48
48
  export { sanitizeInput, isValidUrl, isValidUUID, isValidMessageContent, escapeHtml } from './utils/validation'
49
49
 
50
50
  // 导出消息渲染器(供自定义使用)
@@ -637,11 +637,13 @@ export interface NewSessionRequest {
637
637
  site?: string
638
638
  channel_code?: string
639
639
  real_user_id?: string
640
+ page_url?: string
640
641
  }
641
642
 
642
643
  export interface NewSessionResponse {
643
644
  success: boolean
644
645
  sessionId: string
646
+ userId?: string // 后端生成的 userId(当请求的 user_id 为空时返回)
645
647
  message: string
646
648
  resumed?: boolean
647
649
  messages?: Message[]
@@ -923,8 +925,9 @@ export interface LiveChatWidgetProps {
923
925
 
924
926
  /**
925
927
  * AI 回复促销卡片时触发
928
+ * @param promotions 促销活动数组数据
926
929
  */
927
- onPromotionList?: () => void
930
+ onPromotionList?: (promotions: PromotionItem[]) => void
928
931
 
929
932
  /**
930
933
  * 商品操作回调
@@ -5,14 +5,14 @@
5
5
  * 策略:
6
6
  * 1. 优先从 localStorage 读取
7
7
  * 2. 尝试获取 Google Analytics ID (GAID)
8
- * 3. 兜底:时间戳 + 随机数哈希
8
+ * 3. 返回空字符串,由后端生成
9
9
  */
10
10
 
11
11
  const STORAGE_KEY = 'livechat_user_id'
12
12
 
13
13
  /**
14
14
  * 获取用户唯一标识符(异步版本)
15
- * @returns userId (GAID 或哈希值)
15
+ * @returns userId (GAID 或空字符串,空字符串由后端生成)
16
16
  */
17
17
  export async function getUserId(): Promise<string> {
18
18
  // 1. 尝试从 localStorage 读取
@@ -30,21 +30,17 @@ export async function getUserId(): Promise<string> {
30
30
  return gaid
31
31
  }
32
32
 
33
- // 3. 兜底:使用 SHA-256 生成哈希
34
- try {
35
- const fallback = await generateHashedUserId()
36
- if (typeof window !== 'undefined') {
37
- localStorage.setItem(STORAGE_KEY, fallback)
38
- }
39
- return fallback
40
- } catch (error) {
41
- // 如果 SHA-256 失败,使用同步兜底方案
42
- console.warn('[LiveChat userId] SHA-256 failed, using sync fallback:', error)
43
- const fallback = generateHashedUserIdSync()
44
- if (typeof window !== 'undefined') {
45
- localStorage.setItem(STORAGE_KEY, fallback)
46
- }
47
- return fallback
33
+ // 3. 返回空字符串,由后端生成
34
+ return ''
35
+ }
36
+
37
+ /**
38
+ * 保存后端返回的 userId 到 localStorage
39
+ * @param id 后端生成的 userId
40
+ */
41
+ export function saveUserId(id: string): void {
42
+ if (id && typeof window !== 'undefined') {
43
+ localStorage.setItem(STORAGE_KEY, id)
48
44
  }
49
45
  }
50
46
 
@@ -85,51 +81,6 @@ function getGAID(): string | null {
85
81
  }
86
82
  }
87
83
 
88
- /**
89
- * 生成哈希用户 ID(兜底方案)
90
- * @returns 格式为 user-{hash} 的用户 ID,hash 由 timestamp + random 通过 SHA-256 生成
91
- */
92
- async function generateHashedUserId(): Promise<string> {
93
- const timestamp = Date.now()
94
- const random = Math.random().toString(36).substring(2, 15) // 生成随机字符串
95
-
96
- // 将 timestamp 和 random 组合成字符串
97
- const rawString = `${timestamp}${random}`
98
-
99
- // 使用 Web Crypto API 生成 SHA-256 哈希
100
- const encoder = new TextEncoder()
101
- const data = encoder.encode(rawString)
102
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
103
-
104
- // 将 ArrayBuffer 转换为 16 进制字符串
105
- const hashArray = Array.from(new Uint8Array(hashBuffer))
106
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
107
-
108
- // 取前 16 位作为用户 ID(保持简洁)
109
- return `user-${hashHex.substring(0, 16)}`
110
- }
111
-
112
- /**
113
- * 同步版本的哈希用户 ID 生成(兜底的兜底)
114
- * 当 Web Crypto API 不可用时使用
115
- */
116
- function generateHashedUserIdSync(): string {
117
- const timestamp = Date.now()
118
- const random = Math.random().toString(36).substring(2, 15)
119
- const rawString = `${timestamp}${random}`
120
-
121
- // 使用简单的哈希算法作为兜底
122
- let hash = 0
123
- for (let i = 0; i < rawString.length; i++) {
124
- const char = rawString.charCodeAt(i)
125
- hash = (hash << 5) - hash + char
126
- hash = hash & hash
127
- }
128
-
129
- const hashString = Math.abs(hash).toString(16).padStart(8, '0')
130
- return `user-${hashString}`
131
- }
132
-
133
84
  /**
134
85
  * 清除保存的 userId(用于测试或重置)
135
86
  */
@@ -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,8 +147,8 @@ type Story = StoryObj<typeof LiveChatWidget>
147
147
  export const Default: Story = {
148
148
  args: {
149
149
  // 基础配置
150
- loginUserId: 'test_test',
151
- apiBaseUrl: 'http://172.16.38.183:3003',
150
+ loginUserId: 'test_test1',
151
+ apiBaseUrl: 'https://beta-api-v2-livechat.anker.com',
152
152
  site: 'beta.eufy.com',
153
153
  channelCode: 'dtc',
154
154
  title: 'eufy AI Assistant',
@@ -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 配置(取消注释以启用)