@anker-in/campaign-ui 0.2.11-beta.30 → 0.2.11-beta.32

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 (84) 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/ChatInput.js +1 -1
  4. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  5. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
  6. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  7. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
  8. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +2 -2
  9. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  10. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +2 -2
  11. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +1 -1
  12. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +2 -2
  13. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +3 -1
  14. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -1
  15. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  16. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +1 -0
  17. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  18. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  19. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  20. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +2 -2
  21. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  22. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  23. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  24. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +2 -2
  25. package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
  26. package/dist/cjs/components/LiveChatWidget/constants.js.map +2 -2
  27. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +5 -1
  28. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  29. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  30. package/dist/cjs/components/LiveChatWidget/types.d.ts +26 -0
  31. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  32. package/dist/cjs/stories/LiveChatWidget.stories.d.ts +8 -0
  33. package/dist/cjs/stories/LiveChatWidget.stories.js +21 -4
  34. package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
  35. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  36. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  37. package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
  38. package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  39. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
  40. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  41. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
  42. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +2 -2
  43. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  44. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +2 -2
  45. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +1 -1
  46. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +2 -2
  47. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +3 -1
  48. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -1
  49. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  50. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +1 -0
  51. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  52. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  53. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  54. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +2 -2
  55. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  56. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  57. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  58. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +2 -2
  59. package/dist/esm/components/LiveChatWidget/constants.js +1 -1
  60. package/dist/esm/components/LiveChatWidget/constants.js.map +2 -2
  61. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +5 -1
  62. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  63. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  64. package/dist/esm/components/LiveChatWidget/types.d.ts +26 -0
  65. package/dist/esm/stories/LiveChatWidget.stories.d.ts +8 -0
  66. package/dist/esm/stories/LiveChatWidget.stories.js +24 -7
  67. package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
  68. package/package.json +1 -1
  69. package/src/components/LiveChatWidget/LiveChatWidget.tsx +97 -48
  70. package/src/components/LiveChatWidget/components/ChatInput.tsx +26 -1
  71. package/src/components/LiveChatWidget/components/ChatMessage.tsx +11 -4
  72. package/src/components/LiveChatWidget/components/ChatWindow.tsx +2 -0
  73. package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +1 -1
  74. package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +1 -1
  75. package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +12 -4
  76. package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +23 -6
  77. package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +1 -1
  78. package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +1 -1
  79. package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +2 -2
  80. package/src/components/LiveChatWidget/constants.ts +12 -5
  81. package/src/components/LiveChatWidget/hooks/useChatState.ts +141 -10
  82. package/src/components/LiveChatWidget/types.ts +27 -0
  83. package/src/stories/LiveChatWidget.stories.tsx +182 -0
  84. package/src/styles/livechat.css +1 -2
@@ -71,7 +71,7 @@ export const createQuickRepliesRenderer = (onReplyClick?: (reply: QuickReply) =>
71
71
  key={reply.id}
72
72
  type="button"
73
73
  onClick={() => handleClick(reply)}
74
- className="livechat-quick-reply-button inline-flex font-bold items-center gap-1 rounded-[19px] px-3 py-[6px] text-sm leading-[140%] tracking-[-0.02em] transition-transform hover:scale-105 active:scale-95"
74
+ className="livechat-quick-reply-button inline-flex font-bold items-center gap-1 rounded-[19px] px-3 py-[6px] text-sm leading-[140%] tracking-[-0.02em] transition-transform"
75
75
  >
76
76
  {/* 图标(可选) */}
77
77
  {reply.icon && <span className="text-base">{reply.icon}</span>}
@@ -69,7 +69,7 @@ export const TextBlock: MessageRenderer = {
69
69
  // 自定义段落样式
70
70
  p: ({ node, ...props }) => <p {...props} className="last:mb-0" />,
71
71
  // 自定义列表样式
72
- ul: ({ node, ...props }) => <ul {...props} className="mb-2 ml-4 list-disc" />,
72
+ ul: ({ node, ...props }) => <ul {...props} className="ml-4 list-disc" />,
73
73
  ol: ({ node, ...props }) => <ol {...props} className="mb-2 ml-4 list-decimal" />,
74
74
  li: ({ node, ...props }) => <li {...props} className="mb-1" />,
75
75
  // 自定义标题样式
@@ -77,7 +77,7 @@ export const TextBlock: MessageRenderer = {
77
77
  h2: ({ node, ...props }) => <h2 {...props} className="mb-2 text-base font-bold" />,
78
78
  h3: ({ node, ...props }) => <h3 {...props} className="mb-1 text-sm font-bold" />,
79
79
  // 自定义强调样式
80
- strong: ({ node, ...props }) => <strong {...props} className="font-semibold" />,
80
+ strong: ({ node, ...props }) => <strong {...props} className="font-bold" />,
81
81
  em: ({ node, ...props }) => <em {...props} className="italic" />,
82
82
  }}
83
83
  >
@@ -9,11 +9,18 @@ import type { CommonText } from './types'
9
9
  * 用于将货币代码转换为对应的符号
10
10
  */
11
11
  export const CURRENCY_SYMBOLS: Record<string, string> = {
12
- USD: '$',
13
- EUR: '€',
14
- GBP: '£',
15
- JPY: '¥',
16
- CNY: '¥',
12
+ USD: "$",
13
+ CAD: "CA$",
14
+ GBP: "£",
15
+ EUR: "€",
16
+ AUD: "$",
17
+ SGD: "$",
18
+ NZD: "NZ$",
19
+ AED: "AED ",
20
+ VND: "₫",
21
+ TWD: "NT$",
22
+ PLN: "zł",
23
+ RON: "Lei"
17
24
  }
18
25
 
19
26
  /**
@@ -21,13 +21,17 @@ import { transformCartData } from '../utils/cartTransformers'
21
21
  *
22
22
  * @param buffer 当前缓冲区内容(包含新接收的文本)
23
23
  * @param productMap shopifyId → Product 的映射表
24
+ * @param rawProductMap shopifyId → raw backend product 的映射表
24
25
  * @param onAddToCart 添加到购物车回调
26
+ * @param productCardRender 自定义产品卡片渲染函数
25
27
  * @returns { contents: 需要添加的内容数组, remainingBuffer: 剩余缓冲区内容 }
26
28
  */
27
29
  function parseStreamingText(
28
30
  buffer: string,
29
31
  productMap: Map<string, Product>,
30
- onAddToCart?: (product: Product) => void
32
+ rawProductMap: Map<string, any>,
33
+ onAddToCart?: (product: Product) => void,
34
+ productCardRender?: (product: any) => React.ReactNode
31
35
  ): { contents: MessageContent[]; remainingBuffer: string } {
32
36
  const contents: MessageContent[] = []
33
37
  const regex = /\{\{(?:product:)?([^}]+)\}\}/g
@@ -49,12 +53,18 @@ function parseStreamingText(
49
53
  // 提取产品 ID 并创建产品卡片
50
54
  const productId = match[1].trim()
51
55
  const product = productMap.get(productId)
56
+ const rawProduct = rawProductMap.get(productId)
52
57
 
53
58
  if (product) {
54
59
  console.log('[useChatState] 🎯 实时检测到产品:', productId, '→', product.title)
55
60
  contents.push({
56
61
  type: 'product_card',
57
- data: { product: product, onAddToCart: onAddToCart }
62
+ data: {
63
+ product: product,
64
+ rawProduct: rawProduct,
65
+ onAddToCart: onAddToCart,
66
+ productCardRender: productCardRender
67
+ }
58
68
  } as ProductCardContent)
59
69
  } else {
60
70
  // 产品未找到,保留原始文本
@@ -98,6 +108,7 @@ function parseStreamingText(
98
108
  * @param text 包含 {{shopifyId}} 标记的文本
99
109
  * @param productMap shopifyId → Product 的映射表
100
110
  * @param onAddToCart 添加到购物车回调
111
+ * @param productCardRender 自定义产品卡片渲染函数
101
112
  * @returns MessageContent 数组
102
113
  *
103
114
  * @example
@@ -114,7 +125,9 @@ function parseStreamingText(
114
125
  function parseTextWithProductIds(
115
126
  text: string,
116
127
  productMap: Map<string, Product>,
117
- onAddToCart?: (product: Product) => void
128
+ rawProductMap: Map<string, any>,
129
+ onAddToCart?: (product: Product) => void,
130
+ productCardRender?: (product: any) => React.ReactNode
118
131
  ): MessageContent[] {
119
132
  const result: MessageContent[] = []
120
133
  // 修改正则表达式以匹配 {{product:ID}} 格式
@@ -139,13 +152,16 @@ function parseTextWithProductIds(
139
152
 
140
153
  // 添加产品卡片
141
154
  const product = productMap.get(productId)
155
+ const rawProduct = rawProductMap.get(productId)
142
156
  if (product) {
143
157
  console.log(`[useChatState] ✅ 找到产品匹配: ${productId} → ${product.title}`)
144
158
  result.push({
145
159
  type: 'product_card',
146
160
  data: {
147
161
  product: product,
162
+ rawProduct: rawProduct,
148
163
  onAddToCart: onAddToCart,
164
+ productCardRender: productCardRender,
149
165
  },
150
166
  } as ProductCardContent)
151
167
  } else {
@@ -187,12 +203,15 @@ function parseTextWithProductIds(
187
203
  * @param contents 原始消息内容数组
188
204
  * @param productMap shopifyId → Product 的映射表
189
205
  * @param onAddToCart 添加到购物车回调
206
+ * @param productCardRender 自定义产品卡片渲染函数
190
207
  * @returns 重组后的消息内容数组
191
208
  */
192
209
  function reorganizeMessageContent(
193
210
  contents: MessageContent[],
194
211
  productMap: Map<string, Product>,
195
- onAddToCart?: (product: Product) => void
212
+ rawProductMap: Map<string, any>,
213
+ onAddToCart?: (product: Product) => void,
214
+ productCardRender?: (product: any) => React.ReactNode
196
215
  ): MessageContent[] {
197
216
  const result: MessageContent[] = []
198
217
 
@@ -200,7 +219,7 @@ function reorganizeMessageContent(
200
219
  // 只处理文本类型
201
220
  if (content.type === 'text') {
202
221
  const textContent = content as TextContent
203
- const segments = parseTextWithProductIds(textContent.text, productMap, onAddToCart)
222
+ const segments = parseTextWithProductIds(textContent.text, productMap, rawProductMap, onAddToCart, productCardRender)
204
223
  result.push(...segments)
205
224
  }
206
225
  // 跳过 product_list(已经被拆分到文本中)
@@ -222,11 +241,13 @@ function reorganizeMessageContent(
222
241
  *
223
242
  * @param message 原始消息
224
243
  * @param onAddToCart 添加到购物车回调
244
+ * @param productCardRender 自定义产品卡片渲染函数
225
245
  * @returns 重组后的消息(如果需要重组),否则返回原消息
226
246
  */
227
247
  function maybeReorganizeHistoricalMessage(
228
248
  message: Message,
229
- onAddToCart?: (product: Product) => void
249
+ onAddToCart?: (product: Product) => void,
250
+ productCardRender?: (product: any) => React.ReactNode
230
251
  ): Message {
231
252
  // 检查消息是否包含 product_list
232
253
  const hasProductList = message.content.some(c => c.type === 'product_list')
@@ -246,6 +267,37 @@ function maybeReorganizeHistoricalMessage(
246
267
 
247
268
  // 构建产品映射
248
269
  const productMap = new Map<string, Product>()
270
+ // 从 structured_content 中提取原始后端产品数据
271
+ const rawProductMap = new Map<string, any>()
272
+
273
+ // 优先从 structured_content 获取原始产品数据(如果存在)
274
+ if (message.structured_content) {
275
+ message.structured_content.forEach(structuredContent => {
276
+ if (structuredContent.type === 'product_list' && Array.isArray(structuredContent.data)) {
277
+ structuredContent.data.forEach((rawProduct: any) => {
278
+ if (rawProduct && rawProduct.shopify_product_id) {
279
+ const shopifyId = rawProduct.shopify_product_id
280
+ // 1. 使用完整的 shopifyId 作为key
281
+ rawProductMap.set(shopifyId, rawProduct)
282
+
283
+ // 2. 同时提取数字部分作为key
284
+ const numericId = shopifyId.match(/\d+$/)?.[0]
285
+ if (numericId) {
286
+ rawProductMap.set(numericId, rawProduct)
287
+ }
288
+
289
+ console.log('[useChatState] 从 structured_content 提取原始产品数据:', {
290
+ fullId: shopifyId,
291
+ numericId,
292
+ title: rawProduct.title,
293
+ })
294
+ }
295
+ })
296
+ }
297
+ })
298
+ }
299
+
300
+ // 构建转换后的产品映射(用于默认渲染)
249
301
  message.content.forEach(content => {
250
302
  if (content.type === 'product_list') {
251
303
  const productListContent = content as ProductListContent
@@ -265,7 +317,7 @@ function maybeReorganizeHistoricalMessage(
265
317
  })
266
318
 
267
319
  // 重组消息内容
268
- const reorganizedContent = reorganizeMessageContent(message.content, productMap, onAddToCart)
320
+ const reorganizedContent = reorganizeMessageContent(message.content, productMap, rawProductMap, onAddToCart, productCardRender)
269
321
 
270
322
  // 返回新消息对象
271
323
  return {
@@ -346,6 +398,11 @@ export interface UseChatStateOptions {
346
398
  * 购物车按钮点击回调
347
399
  */
348
400
  onCart?: (cartId: string, checkoutUrl?: string) => void
401
+
402
+ /**
403
+ * 自定义产品卡片渲染函数
404
+ */
405
+ productCardRender?: (product: Product) => React.ReactNode
349
406
  }
350
407
 
351
408
  export interface UseChatStateReturn {
@@ -458,6 +515,7 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
458
515
  onPromotionList,
459
516
  onAddToCart,
460
517
  onCart,
518
+ productCardRender,
461
519
  } = options
462
520
 
463
521
  // 会话管理
@@ -507,9 +565,15 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
507
565
  // 产品映射缓存 (shopifyId → Product),用于实时解析占位符
508
566
  const productMapRef = useRef<Map<string, Product>>(new Map())
509
567
 
568
+ // 原始产品数据缓存 (shopifyId → raw backend product),用于 productCardRender
569
+ const rawProductMapRef = useRef<Map<string, any>>(new Map())
570
+
510
571
  // 文本缓冲区,用于存储未完成的文本(处理占位符跨越多个 delta 的情况)
511
572
  const textBufferRef = useRef<string>('')
512
573
 
574
+ // 超时定时器 ref,用于处理 3 分钟无响应的情况
575
+ const timeoutTimerRef = useRef<NodeJS.Timeout | null>(null)
576
+
513
577
  /**
514
578
  * 打开聊天窗口
515
579
  */
@@ -572,11 +636,11 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
572
636
  }
573
637
 
574
638
  // 对每条历史消息进行重组(如果需要)
575
- const reorganizedMessages = validMessages.map(msg => maybeReorganizeHistoricalMessage(msg, onAddToCart))
639
+ const reorganizedMessages = validMessages.map(msg => maybeReorganizeHistoricalMessage(msg, onAddToCart, productCardRender))
576
640
 
577
641
  setMessagesState(reorganizedMessages)
578
642
  },
579
- [onAddToCart]
643
+ [onAddToCart, productCardRender]
580
644
  )
581
645
 
582
646
  /**
@@ -655,6 +719,12 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
655
719
  const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')
656
720
  if (hasThinking) {
657
721
  currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')
722
+
723
+ // 清除超时定时器(已收到内容,不再需要超时提示)
724
+ if (timeoutTimerRef.current) {
725
+ clearTimeout(timeoutTimerRef.current)
726
+ timeoutTimerRef.current = null
727
+ }
658
728
  }
659
729
 
660
730
  // 将新文本添加到缓冲区
@@ -664,7 +734,9 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
664
734
  const { contents, remainingBuffer } = parseStreamingText(
665
735
  textBufferRef.current,
666
736
  productMapRef.current,
667
- onAddToCart
737
+ rawProductMapRef.current,
738
+ onAddToCart,
739
+ productCardRender
668
740
  )
669
741
 
670
742
  // 更新缓冲区为剩余内容
@@ -736,6 +808,21 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
736
808
  // 触发商品列表回调
737
809
  onProductList?.()
738
810
 
811
+ // 缓存原始后端产品数据(用于 productCardRender)
812
+ contentData.forEach((rawProduct: any) => {
813
+ if (rawProduct && rawProduct.shopify_product_id) {
814
+ const shopifyId = rawProduct.shopify_product_id
815
+ // 1. 使用完整的 shopifyId 作为key
816
+ rawProductMapRef.current.set(shopifyId, rawProduct)
817
+
818
+ // 2. 同时提取数字部分作为key
819
+ const numericId = shopifyId.match(/\d+$/)?.[0]
820
+ if (numericId) {
821
+ rawProductMapRef.current.set(numericId, rawProduct)
822
+ }
823
+ }
824
+ })
825
+
739
826
  // 使用统一的产品转换工具函数
740
827
  const transformedProducts = transformProducts(contentData, site)
741
828
 
@@ -908,6 +995,7 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
908
995
  // 清空缓冲区和产品映射
909
996
  textBufferRef.current = ''
910
997
  productMapRef.current.clear()
998
+ rawProductMapRef.current.clear()
911
999
  currentMessageRef.current = null
912
1000
  break
913
1001
  }
@@ -960,6 +1048,39 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
960
1048
  case 'done': {
961
1049
  // 流结束
962
1050
  setIsStreaming(false)
1051
+
1052
+ // 检查当前消息是否还存在 thinking block
1053
+ if (currentMessageRef.current) {
1054
+ const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')
1055
+
1056
+ if (hasThinking) {
1057
+ // 如果还存在 thinking block,设置 3 分钟超时
1058
+ const currentMessage = currentMessageRef.current
1059
+
1060
+ timeoutTimerRef.current = setTimeout(() => {
1061
+ setMessagesState(prev => {
1062
+ return prev.map(msg => {
1063
+ if (msg.id === currentMessage.id) {
1064
+ // 移除 thinking block,添加超时提示
1065
+ const contentWithoutThinking = msg.content.filter(c => c.type !== 'thinking')
1066
+ return {
1067
+ ...msg,
1068
+ content: [
1069
+ ...contentWithoutThinking,
1070
+ {
1071
+ type: 'text',
1072
+ text: 'Session timed out, please try again.',
1073
+ } as TextContent,
1074
+ ],
1075
+ }
1076
+ }
1077
+ return msg
1078
+ })
1079
+ })
1080
+ }, 3 * 60 * 1000) // 3 分钟
1081
+ }
1082
+ }
1083
+
963
1084
  currentMessageRef.current = null
964
1085
  break
965
1086
  }
@@ -986,6 +1107,16 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
986
1107
  ]
987
1108
  )
988
1109
 
1110
+ // 组件卸载时清理超时定时器
1111
+ useEffect(() => {
1112
+ return () => {
1113
+ if (timeoutTimerRef.current) {
1114
+ clearTimeout(timeoutTimerRef.current)
1115
+ timeoutTimerRef.current = null
1116
+ }
1117
+ }
1118
+ }, [])
1119
+
989
1120
  return {
990
1121
  messages,
991
1122
  isOpen,
@@ -43,6 +43,10 @@ export interface Message {
43
43
  content: MessageContent[]
44
44
  timestamp: number
45
45
  metadata?: MessageMetadata
46
+ structured_content?: Array<{
47
+ type: string
48
+ data: any
49
+ }>
46
50
  }
47
51
 
48
52
  // ============================================================================
@@ -83,7 +87,9 @@ export interface ProductCardContent {
83
87
  type: 'product_card'
84
88
  data: {
85
89
  product: Product
90
+ rawProduct?: any // Raw backend product data (for custom render)
86
91
  onAddToCart?: (product: Product) => void
92
+ productCardRender?: (product: any) => React.ReactNode
87
93
  }
88
94
  }
89
95
 
@@ -596,6 +602,7 @@ export interface StatusData {
596
602
  export interface ErrorData {
597
603
  message: string
598
604
  code?: string
605
+ type?: string
599
606
  }
600
607
 
601
608
  // ============================================================================
@@ -899,6 +906,26 @@ export interface LiveChatWidgetProps {
899
906
  * 用于自定义按钮文案
900
907
  */
901
908
  commonText?: CommonText
909
+
910
+ /**
911
+ * 自定义产品卡片渲染函数
912
+ * 用于自定义渲染 product_card 类型的产品卡片
913
+ * 当提供此函数时,将替代默认的产品卡片渲染逻辑
914
+ * @param product 原始后端产品数据(未经转换的数据)
915
+ * @returns React 可渲染的内容
916
+ * @example
917
+ * ```tsx
918
+ * <LiveChatWidget
919
+ * productCardRender={(product) => (
920
+ * <div>
921
+ * <h3>{product.title}</h3>
922
+ * <p>{product.price_range?.min}</p>
923
+ * </div>
924
+ * )}
925
+ * />
926
+ * ```
927
+ */
928
+ productCardRender?: (product: any) => React.ReactNode
902
929
  }
903
930
 
904
931
  export interface MessageRenderer {
@@ -570,3 +570,185 @@ export const WithRecaptcha: Story = {
570
570
  },
571
571
  },
572
572
  }
573
+
574
+ /**
575
+ * 自定义产品卡片渲染
576
+ *
577
+ * 演示如何使用 productCardRender 自定义产品卡片的渲染样式。
578
+ *
579
+ * 当后端返回产品占位符 {{product:xxx}} 时,会使用自定义渲染函数来展示产品卡片。
580
+ */
581
+ export const CustomProductCardRender: Story = {
582
+ args: {
583
+ loginUserId: 'test_test',
584
+ apiBaseUrl: 'http://172.16.38.183:3003',
585
+ cartId: 'gid://shopify/Cart/hWN7wB3Pa12gh78d8hPOAUBI?key=0e73db1d3fb5ac21da19099c45033253',
586
+ accessToken: '47b1aa2c0797043f9baba39388029d70',
587
+ title: 'eufy',
588
+ site: 'beta.eufy.com',
589
+ channelCode: 'dtc',
590
+ welcomeMessage: '你好!我可以为你推荐产品,试试发送 "推荐产品" 来查看自定义产品卡片效果。',
591
+
592
+ // 自定义产品卡片渲染函数
593
+ // 注意:product 参数是原始后端数据(未经转换)
594
+ productCardRender: (product) => {
595
+ // 原始后端数据使用 snake_case 字段名
596
+ const imageUrl = product.featured_image || ''
597
+ const title = product.title || ''
598
+ const description = product.description || ''
599
+ const price = product.price_range?.min || product.variants?.[0]?.price || 0
600
+ const currency = product.price_range?.currency || product.variants?.[0]?.currency || 'USD'
601
+ const averageRating = product.average_rating
602
+
603
+ return (
604
+ <div
605
+ style={{
606
+ border: '2px solid #4CAF50',
607
+ borderRadius: '16px',
608
+ padding: '20px',
609
+ margin: '12px 0',
610
+ backgroundColor: '#f0f9ff',
611
+ boxShadow: '0 4px 12px rgba(76, 175, 80, 0.15)',
612
+ transition: 'transform 0.2s',
613
+ }}
614
+ onMouseEnter={(e) => {
615
+ e.currentTarget.style.transform = 'scale(1.02)'
616
+ }}
617
+ onMouseLeave={(e) => {
618
+ e.currentTarget.style.transform = 'scale(1)'
619
+ }}
620
+ >
621
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
622
+ <div
623
+ style={{
624
+ width: '70px',
625
+ height: 'auto',
626
+ flexShrink: 0,
627
+ borderRadius: '12px',
628
+ overflow: 'hidden',
629
+ border: '2px solid #e0e0e0',
630
+ }}
631
+ >
632
+ <img
633
+ src={imageUrl}
634
+ alt={title}
635
+ style={{
636
+ width: '100%',
637
+ height: '100%',
638
+ objectFit: 'cover',
639
+ }}
640
+ />
641
+ </div>
642
+
643
+ <div style={{ flex: 1 }}>
644
+ <h3
645
+ style={{
646
+ margin: '0 0 8px 0',
647
+ fontSize: '18px',
648
+ fontWeight: 'bold',
649
+ color: '#1a1a1a',
650
+ lineHeight: '1.4',
651
+ }}
652
+ >
653
+ {title}
654
+ </h3>
655
+
656
+ {description && (
657
+ <p
658
+ style={{
659
+ margin: '0 0 12px 0',
660
+ fontSize: '14px',
661
+ color: '#666',
662
+ lineHeight: '1.5',
663
+ display: '-webkit-box',
664
+ WebkitLineClamp: 2,
665
+ WebkitBoxOrient: 'vertical',
666
+ overflow: 'hidden',
667
+ }}
668
+ >
669
+ {description}
670
+ </p>
671
+ )}
672
+
673
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
674
+ <div
675
+ style={{
676
+ fontSize: '24px',
677
+ fontWeight: 'bold',
678
+ color: '#4CAF50',
679
+ }}
680
+ >
681
+ 'test price'
682
+ </div>
683
+
684
+ {averageRating !== undefined && (
685
+ <div
686
+ style={{
687
+ display: 'flex',
688
+ alignItems: 'center',
689
+ gap: '4px',
690
+ fontSize: '14px',
691
+ color: '#666',
692
+ }}
693
+ >
694
+ <span style={{ color: '#FFB800' }}>⭐</span>
695
+ <span>{averageRating.toFixed(1)}</span>
696
+ </div>
697
+ )}
698
+
699
+ <button
700
+
701
+ style={{
702
+ marginLeft: 'auto',
703
+ padding: '8px 20px',
704
+ backgroundColor: '#4CAF50',
705
+ color: 'white',
706
+ borderRadius: '8px',
707
+ textDecoration: 'none',
708
+ fontSize: '14px',
709
+ fontWeight: 'bold',
710
+ transition: 'background-color 0.2s',
711
+ }}
712
+ onClick={() => {
713
+ console.log('View Details', product)
714
+ }}
715
+ >
716
+ View Details
717
+ </button>
718
+ </div>
719
+ </div>
720
+ </div>
721
+ </div>
722
+ )
723
+ },
724
+
725
+ onAddToCart: (product) => {
726
+ console.log('Add to cart:', product)
727
+ alert(`Added ${product.title} to cart!`)
728
+ },
729
+ },
730
+ parameters: {
731
+ docs: {
732
+ description: {
733
+ story: `
734
+ ### 自定义产品卡片渲染
735
+
736
+ 使用 \`productCardRender\` 属性可以完全自定义产品卡片的展示样式。
737
+
738
+ **功能说明:**
739
+
740
+ - 当后端返回带有产品占位符 \`{{product:xxx}}\` 的文本流时,会使用自定义渲染函数
741
+ - 自定义函数接收完整的产品对象作为参数
742
+ - 返回 React 节点,可以是任何合法的 JSX
743
+ - 如果不提供 \`productCardRender\`,则使用默认的产品卡片样式
744
+
745
+ **测试步骤:**
746
+
747
+ 1. 打开聊天窗口
748
+ 2. 发送 "推荐产品" 或任何会触发产品推荐的消息
749
+ 3. 观察返回的产品卡片使用的是自定义样式(绿色边框、悬停放大效果)
750
+ `,
751
+ },
752
+ },
753
+ },
754
+ }
@@ -236,7 +236,6 @@
236
236
  .livechat-markdown ul,
237
237
  .livechat-markdown ol {
238
238
  margin-left: 1.5rem;
239
- margin-bottom: 0.5rem;
240
239
  }
241
240
 
242
241
  .livechat-markdown li {
@@ -261,7 +260,7 @@
261
260
  }
262
261
 
263
262
  .livechat-markdown strong {
264
- font-weight: 600;
263
+ font-weight: 700;
265
264
  }
266
265
 
267
266
  .livechat-markdown em {