@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.
- 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/ChatInput.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
- package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +3 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +1 -0
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
- package/dist/cjs/components/LiveChatWidget/constants.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +5 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/types.d.ts +26 -0
- package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
- package/dist/cjs/stories/LiveChatWidget.stories.d.ts +8 -0
- package/dist/cjs/stories/LiveChatWidget.stories.js +21 -4
- package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
- 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/ChatInput.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
- package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +3 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +1 -0
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/constants.js +1 -1
- package/dist/esm/components/LiveChatWidget/constants.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +5 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/types.d.ts +26 -0
- package/dist/esm/stories/LiveChatWidget.stories.d.ts +8 -0
- package/dist/esm/stories/LiveChatWidget.stories.js +24 -7
- package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
- package/package.json +1 -1
- package/src/components/LiveChatWidget/LiveChatWidget.tsx +97 -48
- package/src/components/LiveChatWidget/components/ChatInput.tsx +26 -1
- package/src/components/LiveChatWidget/components/ChatMessage.tsx +11 -4
- package/src/components/LiveChatWidget/components/ChatWindow.tsx +2 -0
- package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +1 -1
- package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +1 -1
- package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +12 -4
- package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +23 -6
- package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +1 -1
- package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +1 -1
- package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +2 -2
- package/src/components/LiveChatWidget/constants.ts +12 -5
- package/src/components/LiveChatWidget/hooks/useChatState.ts +141 -10
- package/src/components/LiveChatWidget/types.ts +27 -0
- package/src/stories/LiveChatWidget.stories.tsx +182 -0
- 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
|
|
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="
|
|
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-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/styles/livechat.css
CHANGED
|
@@ -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:
|
|
263
|
+
font-weight: 700;
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
.livechat-markdown em {
|