@anker-in/campaign-ui 0.3.3 → 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 (224) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  3. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  4. package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +23 -2
  5. package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
  6. package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
  7. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js +1 -1
  8. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  9. package/dist/cjs/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  10. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js +1 -1
  11. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  12. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
  13. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  14. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  15. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
  16. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  17. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  18. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  19. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  20. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  21. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  22. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  23. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  24. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  25. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  26. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  27. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  28. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  29. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  30. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  31. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  32. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  33. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  34. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  35. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  36. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  37. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  38. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  39. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  40. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  41. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  42. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  43. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js +1 -1
  44. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  45. package/dist/cjs/components/LiveChatWidget/components/MessageList.js +3 -3
  46. package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +3 -3
  47. package/dist/cjs/components/LiveChatWidget/constants.d.ts +5 -0
  48. package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
  49. package/dist/cjs/components/LiveChatWidget/constants.js.map +3 -3
  50. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  51. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  52. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  53. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +36 -2
  54. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  55. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  56. package/dist/cjs/components/LiveChatWidget/index.d.ts +1 -1
  57. package/dist/cjs/components/LiveChatWidget/index.js +1 -1
  58. package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
  59. package/dist/cjs/components/LiveChatWidget/types.d.ts +213 -3
  60. package/dist/cjs/components/LiveChatWidget/types.js +1 -1
  61. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  62. package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  63. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
  64. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  65. package/dist/cjs/components/chat/markdown.js +1 -1
  66. package/dist/cjs/components/chat/markdown.js.map +2 -2
  67. package/dist/cjs/components/index.d.ts +2 -0
  68. package/dist/cjs/components/index.js +1 -1
  69. package/dist/cjs/components/index.js.map +3 -3
  70. package/dist/cjs/stories/LiveChatWidget.stories.d.ts +1 -79
  71. package/dist/cjs/stories/LiveChatWidget.stories.js +3 -49
  72. package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
  73. package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  74. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  75. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  76. package/dist/esm/components/LiveChatWidget/api/chat.d.ts +23 -2
  77. package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
  78. package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
  79. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js +1 -1
  80. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  81. package/dist/esm/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  82. package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
  83. package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  84. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
  85. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  86. package/dist/esm/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  87. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
  88. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  89. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  90. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  91. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  92. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  93. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  94. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  95. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  96. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  97. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  98. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  99. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  100. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  101. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  102. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  103. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  104. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  105. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  106. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  107. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  108. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  109. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  110. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  111. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  112. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  113. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  114. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  115. package/dist/esm/components/LiveChatWidget/components/MessageContent.js +1 -1
  116. package/dist/esm/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  117. package/dist/esm/components/LiveChatWidget/components/MessageList.js +3 -3
  118. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
  119. package/dist/esm/components/LiveChatWidget/constants.d.ts +5 -0
  120. package/dist/esm/components/LiveChatWidget/constants.js +1 -1
  121. package/dist/esm/components/LiveChatWidget/constants.js.map +3 -3
  122. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  123. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  124. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  125. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +36 -2
  126. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  127. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  128. package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
  129. package/dist/esm/components/LiveChatWidget/index.js +1 -1
  130. package/dist/esm/components/LiveChatWidget/index.js.map +2 -2
  131. package/dist/esm/components/LiveChatWidget/types.d.ts +213 -3
  132. package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  133. package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
  134. package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  135. package/dist/esm/components/chat/markdown.js +1 -1
  136. package/dist/esm/components/chat/markdown.js.map +2 -2
  137. package/dist/esm/components/index.d.ts +2 -0
  138. package/dist/esm/components/index.js +1 -1
  139. package/dist/esm/components/index.js.map +3 -3
  140. package/dist/esm/stories/LiveChatWidget.stories.d.ts +1 -79
  141. package/dist/esm/stories/LiveChatWidget.stories.js +3 -49
  142. package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
  143. package/dist/index.d.mts +1305 -0
  144. package/dist/index.d.ts +1305 -0
  145. package/dist/index.js +26656 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/index.mjs +26641 -0
  148. package/dist/index.mjs.map +1 -0
  149. package/package.json +8 -1
  150. package/src/components/LiveChatWidget/LiveChatWidget.tsx +907 -0
  151. package/src/components/LiveChatWidget/api/chat.ts +175 -0
  152. package/src/components/LiveChatWidget/components/ChatBubble.tsx +152 -0
  153. package/src/components/LiveChatWidget/components/ChatHeader.tsx +150 -0
  154. package/src/components/LiveChatWidget/components/ChatInput.tsx +253 -0
  155. package/src/components/LiveChatWidget/components/ChatMessage.tsx +190 -0
  156. package/src/components/LiveChatWidget/components/ChatWindow.tsx +363 -0
  157. package/src/components/LiveChatWidget/components/ComplianceDialog.tsx +216 -0
  158. package/src/components/LiveChatWidget/components/MessageContent/CartCard.tsx +202 -0
  159. package/src/components/LiveChatWidget/components/MessageContent/ErrorBlock.tsx +75 -0
  160. package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +128 -0
  161. package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +152 -0
  162. package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +227 -0
  163. package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +377 -0
  164. package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +293 -0
  165. package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +170 -0
  166. package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +91 -0
  167. package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +110 -0
  168. package/src/components/LiveChatWidget/components/MessageContent/ThinkingBlock.tsx +53 -0
  169. package/src/components/LiveChatWidget/components/MessageContent/index.ts +16 -0
  170. package/src/components/LiveChatWidget/components/MessageContent.tsx +113 -0
  171. package/src/components/LiveChatWidget/components/MessageList.tsx +256 -0
  172. package/src/components/LiveChatWidget/components/ScrollAnchor.tsx +75 -0
  173. package/src/components/LiveChatWidget/constants.ts +36 -0
  174. package/src/components/LiveChatWidget/hooks/useChatAPI.ts +146 -0
  175. package/src/components/LiveChatWidget/hooks/useChatState.ts +1091 -0
  176. package/src/components/LiveChatWidget/hooks/useSession.ts +123 -0
  177. package/src/components/LiveChatWidget/index.tsx +63 -0
  178. package/src/components/LiveChatWidget/types.ts +1012 -0
  179. package/src/components/LiveChatWidget/utils/cartTransformers.ts +72 -0
  180. package/src/components/LiveChatWidget/utils/fetcher.ts +131 -0
  181. package/src/components/LiveChatWidget/utils/messageRenderers.ts +120 -0
  182. package/src/components/LiveChatWidget/utils/productTransformers.ts +149 -0
  183. package/src/components/LiveChatWidget/utils/userId.ts +140 -0
  184. package/src/components/LiveChatWidget/utils/validation.ts +99 -0
  185. package/src/components/chat/markdown.tsx +1 -1
  186. package/src/components/index.ts +23 -0
  187. package/src/stories/LiveChatWidget.stories.tsx +317 -0
  188. package/src/styles/livechat.css +346 -0
  189. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  190. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  191. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  192. package/dist/cjs/components/credits/context/utils/atobID.d.ts +0 -1
  193. package/dist/cjs/components/credits/context/utils/atobID.js +0 -2
  194. package/dist/cjs/components/credits/context/utils/atobID.js.map +0 -7
  195. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  196. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  197. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  198. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  199. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  200. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  201. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  202. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  203. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  204. package/dist/cjs/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  205. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js +0 -2
  206. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js.map +0 -7
  207. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  208. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  209. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  210. package/dist/esm/components/credits/context/utils/atobID.d.ts +0 -1
  211. package/dist/esm/components/credits/context/utils/atobID.js +0 -2
  212. package/dist/esm/components/credits/context/utils/atobID.js.map +0 -7
  213. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  214. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  215. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  216. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  217. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  218. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  219. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  220. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  221. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  222. package/dist/esm/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  223. package/dist/esm/components/credits/context/utils/variantGetCoupon.js +0 -2
  224. package/dist/esm/components/credits/context/utils/variantGetCoupon.js.map +0 -7
@@ -0,0 +1,907 @@
1
+ /**
2
+ * LiveChat 主组件
3
+ * 集成所有子组件,提供完整的聊天功能
4
+ * 基于 specs/livechat-widget/plan.md 的三层架构设计
5
+ */
6
+
7
+ import React, { useEffect, useCallback } from 'react'
8
+ import * as Dialog from '@radix-ui/react-dialog'
9
+ import type {
10
+ LiveChatWidgetProps,
11
+ QuickReply,
12
+ Message,
13
+ MessageContent,
14
+ ChatStreamRequest,
15
+ BackendCartData,
16
+ CommonText,
17
+ } from './types'
18
+ import { DEFAULT_COMMON_TEXT } from './constants'
19
+ import { ChatBubble } from './components/ChatBubble'
20
+ import { ChatWindow } from './components/ChatWindow'
21
+ import { ComplianceDialog } from './components/ComplianceDialog'
22
+ import { useChatState } from './hooks/useChatState'
23
+ import { useChatAPI } from './hooks/useChatAPI'
24
+ import { MessageRendererRegistry } from './utils/messageRenderers'
25
+ import { sanitizeInput } from './utils/validation'
26
+ import { transformProducts } from './utils/productTransformers.js'
27
+ import { transformCartData } from './utils/cartTransformers.js'
28
+ import Cookies from 'js-cookie'
29
+ import {
30
+ TextBlock,
31
+ ProductCard,
32
+ ProductList,
33
+ ProductComparisonRenderer,
34
+ PolicyBlock,
35
+ createQuickRepliesRenderer,
36
+ ThinkingBlock,
37
+ ErrorBlock,
38
+ FAQListRenderer,
39
+ PromotionListRenderer,
40
+ CartCard,
41
+ } from './components/MessageContent/index.js'
42
+
43
+ /**
44
+ * LiveChat 聊天组件
45
+ *
46
+ * 功能:
47
+ * - 气泡弹窗聊天界面
48
+ * - SSE 流式消息接收
49
+ * - 会话管理(userId, sessionId)
50
+ * - 历史消息加载
51
+ * - 多种消息类型渲染
52
+ * - 自定义扩展机制
53
+ * - reCAPTCHA v3 安全防护
54
+ *
55
+ * 架构:
56
+ * - UI Layer: ChatBubble, ChatWindow, MessageList, etc.
57
+ * - Logic Layer: useChatState, useChatAPI, useSession
58
+ * - Core Layer: MessageRendererRegistry, 自定义渲染器
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * // 基础使用(使用默认位置)
63
+ * <LiveChatWidget
64
+ * apiBaseUrl="https://beta-api-livechat.anker.com"
65
+ * site="www.eufy.com"
66
+ * welcomeMessage="你好!我是 AI 助手"
67
+ * onMessageSend={(msg) => console.log('Sent:', msg)}
68
+ * />
69
+ *
70
+ * // 使用自定义位置
71
+ * <LiveChatWidget
72
+ * apiBaseUrl="https://beta-api-livechat.anker.com"
73
+ * site="www.eufy.com"
74
+ * position={{ bottom: "20px", right: "30px" }}
75
+ * onMessageSend={(msg) => console.log('Sent:', msg)}
76
+ * />
77
+ *
78
+ * // 启用 reCAPTCHA v3 安全防护(提供 sitekey 即自动启用)
79
+ * <LiveChatWidget
80
+ * apiBaseUrl="https://beta-api-livechat.anker.com"
81
+ * site="www.eufy.com"
82
+ * recaptchaSitekey="6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14"
83
+ * recaptchaAction="livechat"
84
+ * />
85
+ *
86
+ * // 使用自定义 headers 和 reCAPTCHA
87
+ * <LiveChatWidget
88
+ * apiBaseUrl="https://beta-api-livechat.anker.com"
89
+ * site="www.eufy.com"
90
+ * headers={{
91
+ * "Authorization": "Bearer your-token",
92
+ * "X-Custom-Header": "value"
93
+ * }}
94
+ * recaptchaSitekey="your-site-key"
95
+ * />
96
+ * ```
97
+ */
98
+ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
99
+ apiBaseUrl,
100
+ headers,
101
+ recaptchaSitekey,
102
+ recaptchaAction,
103
+ site,
104
+ channelCode,
105
+ loginUserId,
106
+ cartId,
107
+ accessToken,
108
+ position,
109
+ welcomeMessage,
110
+ quickReplies,
111
+ customRenderers,
112
+ logoUrl,
113
+ title,
114
+ chatBubbleIcon,
115
+ open,
116
+ onOpenChange,
117
+ onOpen,
118
+ onClose,
119
+ onMessageSend,
120
+ onError,
121
+ onTextMessage,
122
+ onProductList,
123
+ onPromotionList,
124
+ onAddToCart,
125
+ onCart,
126
+ showNewSessionButton,
127
+ commonText,
128
+ productCardRender,
129
+ bottomTips,
130
+ complianceConfig,
131
+ }) => {
132
+ // 法规协议弹窗状态
133
+ // 从 Cookie 读取用户是否已同意(有效期 365 天)
134
+ const cookieName = complianceConfig?.cookieName || 'livechat_compliance_agreed'
135
+ const [showComplianceDialog, setShowComplianceDialog] = React.useState(false)
136
+ const [hasAgreedCompliance, setHasAgreedCompliance] = React.useState(() => {
137
+ // 初始化时检查 Cookie
138
+ return complianceConfig ? Cookies.get(cookieName) !== undefined : true
139
+ })
140
+
141
+ // 合并默认文案和自定义文案
142
+ const mergedText: Required<CommonText> = React.useMemo(
143
+ () => ({
144
+ ...DEFAULT_COMMON_TEXT,
145
+ ...commonText,
146
+ }),
147
+ [commonText]
148
+ )
149
+
150
+ // 状态管理
151
+ const chatState = useChatState({
152
+ welcomeMessage,
153
+ site,
154
+ open,
155
+ onOpenChange,
156
+ onOpen,
157
+ onClose,
158
+ onMessageSend,
159
+ onError,
160
+ onTextMessage,
161
+ onProductList,
162
+ onPromotionList,
163
+ onAddToCart,
164
+ onCart,
165
+ productCardRender,
166
+ })
167
+
168
+ const {
169
+ messages,
170
+ isOpen,
171
+ userId,
172
+ sessionId,
173
+ inputValue,
174
+ isStreaming,
175
+ openChat,
176
+ closeChat,
177
+ setInputValue,
178
+ addMessage,
179
+ setMessages,
180
+ clearMessages,
181
+ handleSSEEvent,
182
+ saveSession,
183
+ clearSession,
184
+ } = chatState
185
+
186
+ // 初始化加载状态(用户未配置欢迎语时,显示 loading 直到接口返回)
187
+ const [isInitializing, setIsInitializing] = React.useState(false)
188
+
189
+ // API 调用
190
+ const { sendMessageStream, createSession } = useChatAPI({
191
+ apiBaseUrl,
192
+ headers,
193
+ recaptchaConfig: {
194
+ needRecaptcha: !!recaptchaSitekey, // 根据 sitekey 自动判断是否启用
195
+ recaptchaSitekey,
196
+ recaptchaAction,
197
+ },
198
+ onError,
199
+ })
200
+
201
+ // 使用 ref 存储最新的 handleSendMessage,避免循环依赖
202
+ const handleSendMessageRef = React.useRef<(_message?: string) => Promise<void>>(async (_message?: string) => {})
203
+
204
+ // 消息渲染器注册表
205
+ const rendererRegistry = React.useMemo(() => {
206
+ const registry = new MessageRendererRegistry()
207
+
208
+ // 注册默认渲染器
209
+ registry.register('text', TextBlock)
210
+ registry.register('product_card', ProductCard)
211
+ registry.register('product_list', ProductList)
212
+ registry.register('product_comparison', ProductComparisonRenderer)
213
+ registry.register('policy', PolicyBlock)
214
+ registry.register('thinking', ThinkingBlock)
215
+ registry.register('error', ErrorBlock)
216
+ registry.register('faq_list', FAQListRenderer)
217
+ registry.register('promotion_list', PromotionListRenderer)
218
+ registry.register('cart', CartCard)
219
+
220
+ // 注册快捷回复渲染器(带回调)
221
+ const quickRepliesRenderer = createQuickRepliesRenderer((reply: QuickReply) => {
222
+ // 使用 ref 调用最新的 handleSendMessage
223
+ handleSendMessageRef.current(reply.value)
224
+ })
225
+ registry.register('quick_replies', quickRepliesRenderer)
226
+
227
+ // 注册自定义渲染器
228
+ if (customRenderers) {
229
+ registry.registerMany(customRenderers)
230
+ }
231
+
232
+ return registry
233
+ }, [customRenderers])
234
+
235
+ /**
236
+ * T043: 打开聊天窗口时初始化会话
237
+ * 使用 API v2.0.0 的统一接口:
238
+ * - 如果没有 sessionId,创建新会话
239
+ * - 如果有 sessionId,恢复会话并加载历史消息
240
+ */
241
+ useEffect(() => {
242
+ if (!isOpen || !userId) return
243
+
244
+ const currentSessionId = sessionId
245
+
246
+ if (!currentSessionId) {
247
+ // 没有会话,创建新会话
248
+ handleCreateNewSession()
249
+ } else {
250
+ // 有会话,尝试恢复会话和历史消息
251
+ handleResumeSession(currentSessionId)
252
+ }
253
+ // eslint-disable-next-line react-hooks/exhaustive-deps
254
+ }, [isOpen, userId])
255
+
256
+ /**
257
+ * 规范化消息格式(确保 content 是数组)
258
+ * 后端返回格式:
259
+ * - content: 字符串(文本内容)
260
+ * - structuredContent: 数组(结构化内容,如产品列表、政策等)
261
+ * 需要合并为统一的 content 数组格式
262
+ */
263
+ const normalizeMessage = useCallback(
264
+ (message: any): Message => {
265
+ // 如果 content 已经是数组,直接返回
266
+ if (Array.isArray(message.content)) {
267
+ return message as Message
268
+ }
269
+
270
+ const contentBlocks: MessageContent[] = []
271
+
272
+ // 处理文本内容
273
+ if (typeof message.content === 'string' && message.content.trim()) {
274
+ contentBlocks.push({
275
+ type: 'text',
276
+ text: message.content,
277
+ })
278
+ }
279
+
280
+ // 处理结构化内容
281
+ // 历史消息格式: structured_content: [{type, data}]
282
+ const structuredData = message.structuredContent || message.structured_content
283
+
284
+ if (Array.isArray(structuredData)) {
285
+ structuredData.forEach((block: any) => {
286
+ if (block.type === 'product_list' && Array.isArray(block.data)) {
287
+ // 转换产品列表数据结构 - data 直接是产品数组
288
+ contentBlocks.push({
289
+ type: 'product_list',
290
+ data: {
291
+ products: transformProducts(block.data, site),
292
+ title: undefined, // 历史消息不包含 title
293
+ commonText: mergedText,
294
+ },
295
+ })
296
+ } else if (block.type === 'quick_replies' && block.data?.replies) {
297
+ contentBlocks.push({
298
+ type: 'quick_replies',
299
+ data: {
300
+ replies: block.data.replies,
301
+ },
302
+ })
303
+ } else if (block.type === 'policy' && block.data?.title && block.data?.content) {
304
+ contentBlocks.push({
305
+ type: 'policy',
306
+ data: {
307
+ title: block.data.title,
308
+ content: block.data.content,
309
+ },
310
+ })
311
+ } else if (block.type === 'product_comparison' && block.data?.products && block.data?.dimensions) {
312
+ // 转换产品对比数据结构并注入 onAddToCart 回调
313
+ contentBlocks.push({
314
+ type: 'product_comparison',
315
+ data: {
316
+ products: transformProducts(block.data.products, site),
317
+ dimensions: block.data.dimensions,
318
+ onAddToCart: onAddToCart,
319
+ commonText: mergedText,
320
+ },
321
+ })
322
+ } else if (block.type === 'faq_list' && block.data?.found !== undefined) {
323
+ // FAQ 列表卡片 - 直接使用后端数据
324
+ contentBlocks.push({
325
+ type: 'faq_list',
326
+ data: block.data,
327
+ })
328
+ } else if (block.type === 'promotion_list' && block.data?.found !== undefined) {
329
+ // 促销活动列表卡片 - 直接使用后端数据
330
+ contentBlocks.push({
331
+ type: 'promotion_list',
332
+ data: {
333
+ ...block.data,
334
+ commonText: mergedText,
335
+ },
336
+ })
337
+ } else if (block.type === 'cart' && block.data?.id !== undefined) {
338
+ // 购物车卡片 - 转换后端数据格式并注入 onCart 回调
339
+ const transformedData = transformCartData(block.data as BackendCartData)
340
+ contentBlocks.push({
341
+ type: 'cart',
342
+ data: {
343
+ ...transformedData,
344
+ onCart: onCart,
345
+ commonText: mergedText,
346
+ },
347
+ })
348
+ } else {
349
+ // 其他类型直接添加
350
+ contentBlocks.push(block)
351
+ }
352
+ })
353
+ }
354
+
355
+ // 如果没有任何内容块,返回空文本块
356
+ if (contentBlocks.length === 0) {
357
+ contentBlocks.push({
358
+ type: 'text',
359
+ text: '',
360
+ })
361
+ }
362
+
363
+ return {
364
+ ...message,
365
+ content: contentBlocks,
366
+ } as Message
367
+ },
368
+ [site, onCart, onAddToCart, mergedText]
369
+ )
370
+
371
+ /**
372
+ * 创建新会话
373
+ */
374
+ const handleCreateNewSession = useCallback(async () => {
375
+ if (!userId) return
376
+
377
+ // 如果用户没有配置欢迎语,显示 loading 状态
378
+ if (!welcomeMessage) {
379
+ setIsInitializing(true)
380
+ }
381
+
382
+ try {
383
+ const response = await createSession({
384
+ user_id: userId,
385
+ site: site,
386
+ channel_code: channelCode,
387
+ real_user_id: loginUserId,
388
+ })
389
+
390
+ if (response.success) {
391
+ // 保存新会话 ID
392
+ saveSession(response.sessionId)
393
+
394
+ // 清空消息列表
395
+ clearMessages()
396
+
397
+ // 使用后端返回的欢迎消息,如果没有则使用 props 中的默认值
398
+ const messageText = response.welcomeMessage || welcomeMessage
399
+
400
+ if (messageText) {
401
+ const welcomeContent: MessageContent[] = [{ type: 'text', text: messageText }]
402
+
403
+ // 使用后端返回的快捷问题,如果没有则使用 props 中的快捷回复
404
+ const questions = response.quickQuestions
405
+ if (questions && questions.length > 0) {
406
+ // 将后端的 quickQuestions (字符串数组) 转换为 QuickReply 格式
407
+ const quickRepliesFromBackend = questions.map((question, index) => ({
408
+ id: `quick-${index}`,
409
+ label: question,
410
+ value: question,
411
+ }))
412
+
413
+ welcomeContent.push({
414
+ type: 'quick_replies',
415
+ data: {
416
+ replies: quickRepliesFromBackend,
417
+ },
418
+ })
419
+ } else if (quickReplies && quickReplies.length > 0) {
420
+ // 如果后端没有返回,使用 props 中的快捷回复
421
+ welcomeContent.push({
422
+ type: 'quick_replies',
423
+ data: {
424
+ replies: quickReplies,
425
+ },
426
+ })
427
+ }
428
+
429
+ addMessage({
430
+ id: `welcome-${Date.now()}`,
431
+ role: 'assistant',
432
+ content: welcomeContent,
433
+ timestamp: Date.now(),
434
+ })
435
+ }
436
+ }
437
+ } catch (error) {
438
+ console.error('[LiveChatWidget] Failed to create new session:', error)
439
+ onError?.(error as Error)
440
+
441
+ // Check if it's a reCAPTCHA error
442
+ const isRecaptcha = (error as any)?.type === 'GoogleRecaptchaError'
443
+
444
+ // 添加错误消息到界面
445
+ addMessage({
446
+ id: `error-${Date.now()}`,
447
+ role: 'system',
448
+ content: [
449
+ {
450
+ type: 'error',
451
+ data: {
452
+ message: isRecaptcha
453
+ ? 'Your session has expired. Please refresh the page and try again.'
454
+ : 'Failed to create session. Please refresh the page and try again.',
455
+ code: isRecaptcha ? 'RECAPTCHA_ERROR' : 'SESSION_CREATE_ERROR',
456
+ },
457
+ },
458
+ ],
459
+ timestamp: Date.now(),
460
+ })
461
+ } finally {
462
+ // 接口返回后关闭 loading 状态
463
+ setIsInitializing(false)
464
+ }
465
+ }, [
466
+ userId,
467
+ site,
468
+ channelCode,
469
+ loginUserId,
470
+ createSession,
471
+ saveSession,
472
+ clearMessages,
473
+ welcomeMessage,
474
+ quickReplies,
475
+ addMessage,
476
+ onError,
477
+ ])
478
+
479
+ /**
480
+ * 恢复会话和历史消息(使用新的 API v2.0.0)
481
+ */
482
+ const handleResumeSession = useCallback(
483
+ async (existingSessionId: string) => {
484
+ // 如果用户没有配置欢迎语,显示 loading 状态
485
+ if (!welcomeMessage) {
486
+ setIsInitializing(true)
487
+ }
488
+
489
+ try {
490
+ const response = await createSession({
491
+ user_id: userId,
492
+ session_id: existingSessionId,
493
+ site: site,
494
+ channel_code: channelCode,
495
+ real_user_id: loginUserId,
496
+ })
497
+
498
+ if (response.success && response.resumed) {
499
+ // 会话恢复成功
500
+
501
+ // 准备欢迎消息(无论是否有历史消息都需要)
502
+ const messageText = response.welcomeMessage || welcomeMessage
503
+ const welcomeContent: MessageContent[] = messageText ? [{ type: 'text', text: messageText }] : []
504
+
505
+ // 添加快捷回复到欢迎消息
506
+ const questions = response.quickQuestions
507
+ if (questions && questions.length > 0) {
508
+ const quickRepliesFromBackend = questions.map((question, index) => ({
509
+ id: `quick-${index}`,
510
+ label: question,
511
+ value: question,
512
+ }))
513
+
514
+ welcomeContent.push({
515
+ type: 'quick_replies',
516
+ data: {
517
+ replies: quickRepliesFromBackend,
518
+ },
519
+ })
520
+ } else if (quickReplies && quickReplies.length > 0) {
521
+ welcomeContent.push({
522
+ type: 'quick_replies',
523
+ data: {
524
+ replies: quickReplies,
525
+ },
526
+ })
527
+ }
528
+
529
+ if (response.messages && response.messages.length > 0) {
530
+ // 有历史消息,规范化并加载(过滤掉 null/undefined 值和空内容消息)
531
+ const normalizedMessages = response.messages
532
+ .filter((msg: any) => msg != null)
533
+ .map(normalizeMessage)
534
+ .filter((msg: Message) => msg.content && msg.content.length > 0 && !(msg.content.length === 1 && msg.content[0].type === 'text' && !msg.content[0].text))
535
+
536
+ // 如果有欢迎消息,将其添加到历史消息的开头
537
+ if (welcomeContent.length > 0) {
538
+ const welcomeMsg: Message = {
539
+ id: `welcome-${Date.now()}`,
540
+ role: 'assistant',
541
+ content: welcomeContent,
542
+ timestamp: Date.now(),
543
+ }
544
+ setMessages([welcomeMsg, ...normalizedMessages])
545
+ } else {
546
+ setMessages(normalizedMessages)
547
+ }
548
+ } else {
549
+ // 没有历史消息,仅显示欢迎消息
550
+ clearMessages()
551
+
552
+ if (welcomeContent.length > 0) {
553
+ addMessage({
554
+ id: `welcome-${Date.now()}`,
555
+ role: 'assistant',
556
+ content: welcomeContent,
557
+ timestamp: Date.now(),
558
+ })
559
+ }
560
+ }
561
+ } else if (!response.resumed) {
562
+ // 会话无效或过期,创建新会话
563
+ clearSession()
564
+ handleCreateNewSession()
565
+ }
566
+ } catch (error) {
567
+ console.error('[LiveChatWidget] Failed to resume session:', error)
568
+
569
+ // Check if it's a reCAPTCHA error
570
+ const isRecaptcha = (error as any)?.type === 'GoogleRecaptchaError'
571
+
572
+ if (isRecaptcha) {
573
+ // reCAPTCHA error - show refresh page message, don't retry
574
+ addMessage({
575
+ id: `error-${Date.now()}`,
576
+ role: 'system',
577
+ content: [
578
+ {
579
+ type: 'error',
580
+ data: {
581
+ message: 'Your session has expired. Please refresh the page and try again.',
582
+ code: 'RECAPTCHA_ERROR',
583
+ },
584
+ },
585
+ ],
586
+ timestamp: Date.now(),
587
+ })
588
+ } else {
589
+ // Other errors - show message and try to create new session
590
+ addMessage({
591
+ id: `error-${Date.now()}`,
592
+ role: 'system',
593
+ content: [
594
+ {
595
+ type: 'error',
596
+ data: {
597
+ message: 'Failed to resume session. Creating a new session...',
598
+ code: 'SESSION_RESUME_ERROR',
599
+ },
600
+ },
601
+ ],
602
+ timestamp: Date.now(),
603
+ })
604
+
605
+ // 恢复失败,清空会话并创建新的
606
+ clearSession()
607
+ handleCreateNewSession()
608
+ }
609
+ } finally {
610
+ // 接口返回后关闭 loading 状态
611
+ setIsInitializing(false)
612
+ }
613
+ },
614
+ [
615
+ userId,
616
+ site,
617
+ channelCode,
618
+ loginUserId,
619
+ createSession,
620
+ setMessages,
621
+ clearSession,
622
+ clearMessages,
623
+ normalizeMessage,
624
+ handleCreateNewSession,
625
+ welcomeMessage,
626
+ quickReplies,
627
+ addMessage,
628
+ ]
629
+ )
630
+
631
+ /**
632
+ * 发送消息
633
+ */
634
+ const handleSendMessage = useCallback(
635
+ async (message?: string, isRetry: boolean = false) => {
636
+ const textToSend = message || inputValue.trim()
637
+
638
+ if (!textToSend) return
639
+
640
+ // 清空输入框(仅在非重试时)
641
+ if (!message && !isRetry) {
642
+ setInputValue('')
643
+ }
644
+
645
+ // 输入验证
646
+ const sanitized = sanitizeInput(textToSend)
647
+ if (!sanitized) {
648
+ onError?.(new Error('Invalid message'))
649
+ return
650
+ }
651
+
652
+ // 添加用户消息到界面(仅在非重试时)
653
+ if (!isRetry) {
654
+ const userMessage = {
655
+ id: `user-${Date.now()}`,
656
+ role: 'user' as const,
657
+ content: [{ type: 'text' as const, text: sanitized }],
658
+ timestamp: Date.now(),
659
+ }
660
+ addMessage(userMessage)
661
+ }
662
+
663
+ // 立即添加思考状态消息,提升用户体验
664
+ const thinkingMessage = {
665
+ id: `thinking-${Date.now()}`,
666
+ role: 'assistant' as const,
667
+ content: [{ type: 'thinking' as const, data: { status: 'thinking' } }],
668
+ timestamp: Date.now(),
669
+ }
670
+ addMessage(thinkingMessage)
671
+
672
+ // 触发消息发送回调(仅在非重试时)
673
+ if (!isRetry) {
674
+ onMessageSend?.(sanitized)
675
+ }
676
+
677
+ // 标记是否检测到会话过期
678
+ let sessionExpiredDetected = false
679
+
680
+ try {
681
+ // 确保有 sessionId,如果没有则先创建会话
682
+ let currentSessionId = sessionId
683
+ if (!currentSessionId) {
684
+ // 没有会话,创建新会话
685
+ const response = await createSession({
686
+ user_id: userId,
687
+ site: site,
688
+ channel_code: channelCode,
689
+ real_user_id: loginUserId,
690
+ })
691
+ if (response.success) {
692
+ currentSessionId = response.sessionId
693
+ saveSession(currentSessionId)
694
+ } else {
695
+ throw new Error('Failed to create session')
696
+ }
697
+ }
698
+
699
+ // 构建请求参数(session_id 现在是必填的)
700
+ const requestPayload: ChatStreamRequest = {
701
+ message: sanitized,
702
+ user_id: userId,
703
+ session_id: currentSessionId,
704
+ context: {
705
+ cartId: cartId,
706
+ accessToken: accessToken,
707
+ real_user_id: loginUserId,
708
+ },
709
+ }
710
+
711
+ // 发送消息到后端
712
+ await sendMessageStream(requestPayload, event => {
713
+ // 处理 SSE 事件
714
+ handleSSEEvent(event)
715
+
716
+ // 特殊处理:会话过期(error 事件且 type 为 validation_error)
717
+ if (event.event === 'error' && event.data.type === 'validation_error') {
718
+ sessionExpiredDetected = true
719
+ clearSession()
720
+ }
721
+ })
722
+
723
+ // 如果检测到会话过期且不是重试,自动创建新会话并重试
724
+ if (sessionExpiredDetected && !isRetry) {
725
+ console.log('[LiveChatWidget] Session expired (validation_error), creating new session and retrying...')
726
+
727
+ // 移除 thinking 消息
728
+ const messagesWithoutThinking = messages.filter(msg => msg.id !== thinkingMessage.id)
729
+ setMessages(messagesWithoutThinking)
730
+
731
+ // 创建新会话
732
+ const response = await createSession({
733
+ user_id: userId,
734
+ site: site,
735
+ channel_code: channelCode,
736
+ real_user_id: loginUserId,
737
+ })
738
+
739
+ if (response.success) {
740
+ saveSession(response.sessionId)
741
+ // 重试发送消息
742
+ await handleSendMessage(sanitized, true)
743
+ } else {
744
+ throw new Error('Failed to recreate session after expiration')
745
+ }
746
+ }
747
+ } catch (error) {
748
+ console.error('[LiveChatWidget] Failed to send message:', error)
749
+ onError?.(error as Error)
750
+
751
+ // Check if it's a reCAPTCHA error
752
+ const isRecaptcha = (error as any)?.type === 'GoogleRecaptchaError'
753
+
754
+ // Determine error message based on error type
755
+ let errorMessage: string
756
+ let errorCode: string
757
+
758
+ if (isRecaptcha) {
759
+ errorMessage = 'Your session has expired. Please refresh the page and try again.'
760
+ errorCode = 'RECAPTCHA_ERROR'
761
+ } else if (sessionExpiredDetected) {
762
+ errorMessage = 'Your session has expired. We tried to reconnect but failed. Please try again.'
763
+ errorCode = 'SESSION_EXPIRED'
764
+ } else {
765
+ errorMessage = 'Failed to send message. Please check your network connection and try again.'
766
+ errorCode = 'NETWORK_ERROR'
767
+ }
768
+
769
+ // 移除刚才添加的 thinking 消息,并添加错误消息
770
+ const messagesWithoutThinking = messages.filter(msg => msg.id !== thinkingMessage.id)
771
+ setMessages([
772
+ ...messagesWithoutThinking,
773
+ {
774
+ id: `error-${Date.now()}`,
775
+ role: 'system',
776
+ content: [
777
+ {
778
+ type: 'error',
779
+ data: {
780
+ message: errorMessage,
781
+ code: errorCode,
782
+ },
783
+ },
784
+ ],
785
+ timestamp: Date.now(),
786
+ },
787
+ ])
788
+ }
789
+ },
790
+ [
791
+ inputValue,
792
+ userId,
793
+ sessionId,
794
+ site,
795
+ channelCode,
796
+ loginUserId,
797
+ cartId,
798
+ accessToken,
799
+ messages,
800
+ setInputValue,
801
+ addMessage,
802
+ setMessages,
803
+ createSession,
804
+ sendMessageStream,
805
+ handleSSEEvent,
806
+ saveSession,
807
+ clearSession,
808
+ onMessageSend,
809
+ onError,
810
+ ]
811
+ )
812
+
813
+ // 更新 ref 以保持最新的 handleSendMessage
814
+ React.useEffect(() => {
815
+ handleSendMessageRef.current = handleSendMessage
816
+ }, [handleSendMessage])
817
+
818
+ /**
819
+ * 处理气泡按钮点击
820
+ * 如果配置了法规协议且用户未同意,先显示法规弹窗
821
+ * 否则直接打开聊天窗口
822
+ */
823
+ const handleBubbleClick = useCallback(() => {
824
+ if (complianceConfig && !hasAgreedCompliance) {
825
+ // 显示法规协议弹窗
826
+ setShowComplianceDialog(true)
827
+ } else {
828
+ // 直接打开聊天窗口
829
+ openChat()
830
+ }
831
+ }, [complianceConfig, hasAgreedCompliance, openChat])
832
+
833
+ /**
834
+ * 处理用户同意法规协议
835
+ */
836
+ const handleComplianceAgree = useCallback(() => {
837
+ // 设置同意状态
838
+ setHasAgreedCompliance(true)
839
+ setShowComplianceDialog(false)
840
+
841
+ // 保存到 Cookie(有效期 365 天)
842
+ Cookies.set(cookieName, 'true', { expires: 365 })
843
+
844
+ // 同意后立即打开聊天窗口
845
+ openChat()
846
+ }, [openChat, cookieName])
847
+
848
+ /**
849
+ * 处理法规弹窗关闭
850
+ */
851
+ const handleComplianceClose = useCallback(() => {
852
+ setShowComplianceDialog(false)
853
+ }, [])
854
+
855
+ return (
856
+ <>
857
+ {/* 法规协议弹窗 */}
858
+ {complianceConfig && (
859
+ <ComplianceDialog
860
+ open={showComplianceDialog}
861
+ config={complianceConfig}
862
+ onAgree={handleComplianceAgree}
863
+ onClose={handleComplianceClose}
864
+ />
865
+ )}
866
+
867
+ {/* 气泡按钮 */}
868
+ <ChatBubble
869
+ position={position}
870
+ onClick={handleBubbleClick}
871
+ visible={!isOpen && !showComplianceDialog}
872
+ iconImageUrl={chatBubbleIcon}
873
+ />
874
+
875
+ {/* 聊天窗口(使用 Radix UI Dialog) */}
876
+ <Dialog.Root open={isOpen} onOpenChange={open => (open ? openChat() : closeChat())}>
877
+ <Dialog.Portal>
878
+ <Dialog.Content
879
+ className="livechat-window-enter"
880
+ style={{
881
+ position: 'fixed',
882
+ zIndex: 9998,
883
+ }}
884
+ >
885
+ <ChatWindow
886
+ messages={messages}
887
+ inputValue={inputValue}
888
+ onInputChange={setInputValue}
889
+ onSend={() => handleSendMessage()}
890
+ onClose={closeChat}
891
+ onNewSession={handleCreateNewSession}
892
+ title={title}
893
+ logoUrl={logoUrl}
894
+ isSending={isStreaming}
895
+ isLoadingHistory={isInitializing}
896
+ rendererRegistry={rendererRegistry}
897
+ inputPlaceholder=""
898
+ onAddToCart={onAddToCart}
899
+ showNewSessionButton={showNewSessionButton}
900
+ bottomTips={bottomTips}
901
+ />
902
+ </Dialog.Content>
903
+ </Dialog.Portal>
904
+ </Dialog.Root>
905
+ </>
906
+ )
907
+ }