@astro-minimax/ai 0.9.0 → 0.9.3

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 (251) hide show
  1. package/README.md +108 -18
  2. package/dist/cache/global-cache.d.ts +6 -2
  3. package/dist/cache/global-cache.d.ts.map +1 -1
  4. package/dist/cache/global-cache.js +24 -9
  5. package/dist/cache/index.d.ts +7 -6
  6. package/dist/cache/index.d.ts.map +1 -1
  7. package/dist/cache/index.js +12 -4
  8. package/dist/cache/injection-cache.d.ts +36 -0
  9. package/dist/cache/injection-cache.d.ts.map +1 -0
  10. package/dist/cache/injection-cache.js +90 -0
  11. package/dist/cache/kv-adapter.d.ts.map +1 -1
  12. package/dist/cache/kv-adapter.js +2 -1
  13. package/dist/cache/memory-adapter.d.ts.map +1 -1
  14. package/dist/cache/memory-adapter.js +2 -1
  15. package/dist/cache/response-cache.d.ts +10 -5
  16. package/dist/cache/response-cache.d.ts.map +1 -1
  17. package/dist/cache/response-cache.js +18 -6
  18. package/dist/components/AIChatContainer.d.ts +2 -2
  19. package/dist/components/AIChatContainer.d.ts.map +1 -1
  20. package/dist/components/AIChatContainer.js +8 -920
  21. package/dist/components/ChatInput.d.ts +15 -0
  22. package/dist/components/ChatInput.d.ts.map +1 -0
  23. package/dist/components/ChatInput.js +72 -0
  24. package/dist/components/ChatPanel.d.ts +1 -1
  25. package/dist/components/ChatPanel.d.ts.map +1 -1
  26. package/dist/components/ChatPanel.js +210 -672
  27. package/dist/components/CodeBlock.d.ts +31 -0
  28. package/dist/components/CodeBlock.d.ts.map +1 -0
  29. package/dist/components/CodeBlock.js +143 -0
  30. package/dist/components/MarkmapBlock.d.ts +4 -0
  31. package/dist/components/MarkmapBlock.d.ts.map +1 -0
  32. package/dist/components/MarkmapBlock.js +180 -0
  33. package/dist/components/MermaidBlock.d.ts +4 -0
  34. package/dist/components/MermaidBlock.d.ts.map +1 -0
  35. package/dist/components/MermaidBlock.js +193 -0
  36. package/dist/components/MessageBubble.d.ts +21 -0
  37. package/dist/components/MessageBubble.d.ts.map +1 -0
  38. package/dist/components/MessageBubble.js +233 -0
  39. package/dist/components/ReasoningBlock.d.ts +6 -0
  40. package/dist/components/ReasoningBlock.d.ts.map +1 -0
  41. package/dist/components/ReasoningBlock.js +11 -0
  42. package/dist/components/RichText.d.ts +41 -0
  43. package/dist/components/RichText.d.ts.map +1 -0
  44. package/dist/components/RichText.js +202 -0
  45. package/dist/components/VizShared.d.ts +57 -0
  46. package/dist/components/VizShared.d.ts.map +1 -0
  47. package/dist/components/VizShared.js +233 -0
  48. package/dist/components/tool-auto-continue.d.ts +5 -0
  49. package/dist/components/tool-auto-continue.d.ts.map +1 -0
  50. package/dist/components/tool-auto-continue.js +33 -0
  51. package/dist/constants.d.ts +61 -0
  52. package/dist/constants.d.ts.map +1 -0
  53. package/dist/constants.js +72 -0
  54. package/dist/data/index.d.ts +4 -3
  55. package/dist/data/index.d.ts.map +1 -1
  56. package/dist/data/index.js +4 -10
  57. package/dist/data/knowledge-types.d.ts +8 -0
  58. package/dist/data/knowledge-types.d.ts.map +1 -0
  59. package/dist/data/knowledge-types.js +14 -0
  60. package/dist/data/metadata-loader.d.ts +4 -28
  61. package/dist/data/metadata-loader.d.ts.map +1 -1
  62. package/dist/data/metadata-loader.js +11 -34
  63. package/dist/data/types.d.ts +17 -2
  64. package/dist/data/types.d.ts.map +1 -1
  65. package/dist/extensions/index.d.ts +5 -0
  66. package/dist/extensions/index.d.ts.map +1 -0
  67. package/dist/extensions/index.js +24 -0
  68. package/dist/extensions/injector.d.ts +14 -0
  69. package/dist/extensions/injector.d.ts.map +1 -0
  70. package/dist/extensions/injector.js +146 -0
  71. package/dist/extensions/loader.d.ts +5 -0
  72. package/dist/extensions/loader.d.ts.map +1 -0
  73. package/dist/extensions/loader.js +45 -0
  74. package/dist/extensions/registry.d.ts +4 -0
  75. package/dist/extensions/registry.d.ts.map +1 -0
  76. package/dist/extensions/registry.js +144 -0
  77. package/dist/extensions/types.d.ts +126 -0
  78. package/dist/extensions/types.d.ts.map +1 -0
  79. package/dist/extensions/types.js +0 -0
  80. package/dist/fact-registry/prompt-injector.d.ts +1 -1
  81. package/dist/fact-registry/prompt-injector.d.ts.map +1 -1
  82. package/dist/fact-registry/prompt-injector.js +2 -1
  83. package/dist/index.d.ts +3 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +3 -2
  86. package/dist/intelligence/citation-guard.d.ts +2 -13
  87. package/dist/intelligence/citation-guard.d.ts.map +1 -1
  88. package/dist/intelligence/citation-guard.js +52 -23
  89. package/dist/intelligence/evidence-analysis.d.ts +24 -16
  90. package/dist/intelligence/evidence-analysis.d.ts.map +1 -1
  91. package/dist/intelligence/evidence-analysis.js +118 -20
  92. package/dist/intelligence/evidence-budget.d.ts +13 -0
  93. package/dist/intelligence/evidence-budget.d.ts.map +1 -0
  94. package/dist/intelligence/evidence-budget.js +49 -0
  95. package/dist/intelligence/index.d.ts +10 -4
  96. package/dist/intelligence/index.d.ts.map +1 -1
  97. package/dist/intelligence/index.js +27 -3
  98. package/dist/intelligence/keyword-extract.d.ts +1 -1
  99. package/dist/intelligence/keyword-extract.d.ts.map +1 -1
  100. package/dist/intelligence/keyword-extract.js +5 -9
  101. package/dist/intelligence/request-interpretation.d.ts +40 -0
  102. package/dist/intelligence/request-interpretation.d.ts.map +1 -0
  103. package/dist/intelligence/request-interpretation.js +71 -0
  104. package/dist/intelligence/response-templates.d.ts +1 -0
  105. package/dist/intelligence/response-templates.d.ts.map +1 -1
  106. package/dist/intelligence/response-templates.js +13 -0
  107. package/dist/prompt/dynamic-layer.d.ts +1 -5
  108. package/dist/prompt/dynamic-layer.d.ts.map +1 -1
  109. package/dist/prompt/dynamic-layer.js +145 -9
  110. package/dist/prompt/prompt-builder.d.ts +1 -1
  111. package/dist/prompt/prompt-builder.d.ts.map +1 -1
  112. package/dist/prompt/prompt-builder.js +5 -1
  113. package/dist/prompt/semi-static-layer.d.ts +1 -1
  114. package/dist/prompt/semi-static-layer.d.ts.map +1 -1
  115. package/dist/prompt/semi-static-layer.js +22 -12
  116. package/dist/prompt/static-layer.d.ts.map +1 -1
  117. package/dist/prompt/static-layer.js +37 -4
  118. package/dist/prompt/types.d.ts +9 -4
  119. package/dist/prompt/types.d.ts.map +1 -1
  120. package/dist/provider-manager/base.d.ts +5 -1
  121. package/dist/provider-manager/base.d.ts.map +1 -1
  122. package/dist/provider-manager/base.js +22 -2
  123. package/dist/provider-manager/config.d.ts.map +1 -1
  124. package/dist/provider-manager/config.js +3 -2
  125. package/dist/provider-manager/index.d.ts +1 -1
  126. package/dist/provider-manager/index.d.ts.map +1 -1
  127. package/dist/provider-manager/index.js +1 -2
  128. package/dist/provider-manager/manager.d.ts +10 -1
  129. package/dist/provider-manager/manager.d.ts.map +1 -1
  130. package/dist/provider-manager/manager.js +26 -10
  131. package/dist/provider-manager/openai.d.ts +2 -2
  132. package/dist/provider-manager/openai.d.ts.map +1 -1
  133. package/dist/provider-manager/openai.js +19 -4
  134. package/dist/provider-manager/types.d.ts +18 -38
  135. package/dist/provider-manager/types.d.ts.map +1 -1
  136. package/dist/provider-manager/workers.d.ts +2 -2
  137. package/dist/provider-manager/workers.d.ts.map +1 -1
  138. package/dist/provider-manager/workers.js +15 -4
  139. package/dist/query/followup.d.ts +7 -0
  140. package/dist/query/followup.d.ts.map +1 -0
  141. package/dist/query/followup.js +46 -0
  142. package/dist/query/intent.d.ts +6 -0
  143. package/dist/query/intent.d.ts.map +1 -0
  144. package/dist/query/intent.js +137 -0
  145. package/dist/query/types.d.ts +8 -0
  146. package/dist/query/types.d.ts.map +1 -0
  147. package/dist/query/types.js +0 -0
  148. package/dist/search/hybrid-search.d.ts +111 -0
  149. package/dist/search/hybrid-search.d.ts.map +1 -0
  150. package/dist/search/hybrid-search.js +326 -0
  151. package/dist/search/index.d.ts +11 -9
  152. package/dist/search/index.d.ts.map +1 -1
  153. package/dist/search/index.js +46 -10
  154. package/dist/search/scoring.d.ts +18 -0
  155. package/dist/search/scoring.d.ts.map +1 -0
  156. package/dist/search/{search-utils.js → scoring.js} +14 -27
  157. package/dist/search/search-api.d.ts +16 -1
  158. package/dist/search/search-api.d.ts.map +1 -1
  159. package/dist/search/search-api.js +118 -15
  160. package/dist/search/search-index.d.ts +2 -2
  161. package/dist/search/search-index.d.ts.map +1 -1
  162. package/dist/search/search-index.js +4 -2
  163. package/dist/search/session-cache.d.ts +4 -10
  164. package/dist/search/session-cache.d.ts.map +1 -1
  165. package/dist/search/session-cache.js +12 -45
  166. package/dist/search/types.d.ts +28 -0
  167. package/dist/search/types.d.ts.map +1 -1
  168. package/dist/search/vector-reranker.d.ts +3 -3
  169. package/dist/search/vector-reranker.d.ts.map +1 -1
  170. package/dist/search/vector-reranker.js +14 -2
  171. package/dist/server/chat-handler.d.ts +86 -1
  172. package/dist/server/chat-handler.d.ts.map +1 -1
  173. package/dist/server/chat-handler.js +835 -401
  174. package/dist/server/chat-message-utils.d.ts +6 -0
  175. package/dist/server/chat-message-utils.d.ts.map +1 -0
  176. package/dist/server/chat-message-utils.js +40 -0
  177. package/dist/server/chat-utils.d.ts +30 -0
  178. package/dist/server/chat-utils.d.ts.map +1 -0
  179. package/dist/server/chat-utils.js +88 -0
  180. package/dist/server/dev-server.js +238 -101
  181. package/dist/server/env-config.d.ts +22 -0
  182. package/dist/server/env-config.d.ts.map +1 -0
  183. package/dist/server/env-config.js +25 -0
  184. package/dist/server/errors.d.ts +1 -0
  185. package/dist/server/errors.d.ts.map +1 -1
  186. package/dist/server/errors.js +14 -7
  187. package/dist/server/index.d.ts +2 -4
  188. package/dist/server/index.d.ts.map +1 -1
  189. package/dist/server/index.js +4 -25
  190. package/dist/server/metadata-init.d.ts +10 -5
  191. package/dist/server/metadata-init.d.ts.map +1 -1
  192. package/dist/server/metadata-init.js +78 -34
  193. package/dist/server/notify.d.ts +12 -11
  194. package/dist/server/notify.d.ts.map +1 -1
  195. package/dist/server/notify.js +46 -48
  196. package/dist/server/prompt-runtime.d.ts +60 -0
  197. package/dist/server/prompt-runtime.d.ts.map +1 -0
  198. package/dist/server/prompt-runtime.js +284 -0
  199. package/dist/server/stream-helpers.d.ts +30 -16
  200. package/dist/server/stream-helpers.d.ts.map +1 -1
  201. package/dist/server/stream-helpers.js +152 -15
  202. package/dist/server/types.d.ts +47 -12
  203. package/dist/server/types.d.ts.map +1 -1
  204. package/dist/structured-output/generator.d.ts +6 -0
  205. package/dist/structured-output/generator.d.ts.map +1 -0
  206. package/dist/structured-output/generator.js +164 -0
  207. package/dist/structured-output/index.d.ts +4 -0
  208. package/dist/structured-output/index.d.ts.map +1 -0
  209. package/dist/structured-output/index.js +6 -0
  210. package/dist/structured-output/schemas/evidence.d.ts +88 -0
  211. package/dist/structured-output/schemas/evidence.d.ts.map +1 -0
  212. package/dist/structured-output/schemas/evidence.js +65 -0
  213. package/dist/structured-output/types.d.ts +69 -0
  214. package/dist/structured-output/types.d.ts.map +1 -0
  215. package/dist/structured-output/types.js +0 -0
  216. package/dist/tools/action-tools.d.ts +63 -0
  217. package/dist/tools/action-tools.d.ts.map +1 -0
  218. package/dist/tools/action-tools.js +158 -0
  219. package/dist/tools/index.d.ts +2 -0
  220. package/dist/tools/index.d.ts.map +1 -0
  221. package/dist/tools/index.js +30 -0
  222. package/dist/utils/i18n.d.ts +1 -1
  223. package/dist/utils/i18n.d.ts.map +1 -1
  224. package/dist/utils/i18n.js +1 -1
  225. package/dist/utils/logger.d.ts +11 -0
  226. package/dist/utils/logger.d.ts.map +1 -0
  227. package/dist/utils/logger.js +36 -0
  228. package/dist/utils/text.d.ts +11 -0
  229. package/dist/utils/text.d.ts.map +1 -0
  230. package/dist/utils/text.js +87 -0
  231. package/dist/utils/url.d.ts +19 -0
  232. package/dist/utils/url.d.ts.map +1 -0
  233. package/dist/utils/url.js +13 -0
  234. package/package.json +46 -12
  235. package/dist/intelligence/intent-detect.d.ts +0 -40
  236. package/dist/intelligence/intent-detect.d.ts.map +0 -1
  237. package/dist/intelligence/intent-detect.js +0 -93
  238. package/dist/providers/index.d.ts +0 -2
  239. package/dist/providers/index.d.ts.map +0 -1
  240. package/dist/providers/index.js +0 -5
  241. package/dist/search/search-utils.d.ts +0 -47
  242. package/dist/search/search-utils.d.ts.map +0 -1
  243. package/dist/stream/index.d.ts +0 -3
  244. package/dist/stream/index.d.ts.map +0 -1
  245. package/dist/stream/index.js +0 -8
  246. package/dist/stream/mock-stream.d.ts +0 -12
  247. package/dist/stream/mock-stream.d.ts.map +0 -1
  248. package/dist/stream/mock-stream.js +0 -26
  249. package/dist/stream/response.d.ts +0 -10
  250. package/dist/stream/response.d.ts.map +0 -1
  251. package/dist/stream/response.js +0 -21
@@ -1,461 +1,36 @@
1
- // src/components/ChatPanel.tsx
2
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
3
2
  import { h, Fragment } from 'preact';
4
3
 
5
4
  import { useChat } from "@ai-sdk/react";
6
5
  import { DefaultChatTransport } from "ai";
7
-
8
- // src/providers/mock.ts
9
- var MOCK_RESPONSES = [
10
- {
11
- patterns: [/astro/i, /框架/],
12
- zh: `Astro \u662F\u4E00\u4E2A\u73B0\u4EE3\u5316\u7684\u9759\u6001\u7AD9\u70B9\u751F\u6210\u5668\uFF0C\u6838\u5FC3\u4F18\u52BF\u662F"\u5C9B\u5C7F\u67B6\u6784"\u2014\u2014\u9ED8\u8BA4\u96F6 JS\uFF0C\u53EA\u5728\u4EA4\u4E92\u7EC4\u4EF6\u4E0A\u52A0\u8F7D\u811A\u672C\u3002\u672C\u535A\u5BA2\u57FA\u4E8E Astro \u6784\u5EFA\u3002
13
-
14
- \u63A8\u8350\u9605\u8BFB\uFF1A
15
- - [\u5FEB\u901F\u4E0A\u624B\uFF1A\u4E24\u79CD\u96C6\u6210\u65B9\u5F0F](/zh/posts/getting-started) \u2014 \u4E86\u89E3\u5982\u4F55\u642D\u5EFA astro-minimax \u535A\u5BA2
16
- - [\u5982\u4F55\u914D\u7F6E\u4E3B\u9898](/zh/posts/how-to-configure-astro-minimax-theme) \u2014 \u81EA\u5B9A\u4E49\u4F60\u7684\u535A\u5BA2\u5916\u89C2
17
-
18
- \u5916\u90E8\u8D44\u6E90\uFF1A
19
- - [Astro \u5B98\u65B9\u6587\u6863](https://docs.astro.build) \u2014 \u6DF1\u5165\u5B66\u4E60 Astro \u6846\u67B6
20
- - [Astro \u4E3B\u9898\u5E02\u573A](https://astro.build/themes/) \u2014 \u53D1\u73B0\u66F4\u591A Astro \u4E3B\u9898`,
21
- en: `Astro is a modern static site generator with an "Islands Architecture" \u2014 zero JS by default, loading scripts only for interactive components. This blog is built with Astro.
22
-
23
- Recommended reading:
24
- - [Getting Started: Two Integration Methods](/en/posts/getting-started) \u2014 Learn how to set up an astro-minimax blog
25
- - [How to Configure the Theme](/en/posts/how-to-configure-astro-minimax-theme) \u2014 Customize your blog
26
-
27
- External resources:
28
- - [Astro Documentation](https://docs.astro.build) \u2014 Learn Astro in depth
29
- - [Astro Themes](https://astro.build/themes/) \u2014 Discover more themes`
30
- },
31
- {
32
- patterns: [/推荐|文章|看什么|读什么|recommend/i],
33
- zh: `\u4EE5\u4E0B\u662F\u4E00\u4E9B\u70ED\u95E8\u6587\u7AE0\u63A8\u8350\uFF1A
34
-
35
- **\u5165\u95E8\u7CFB\u5217\uFF1A**
36
- - [\u5FEB\u901F\u4E0A\u624B\uFF1A\u4E24\u79CD\u96C6\u6210\u65B9\u5F0F](/zh/posts/getting-started) \u2014 \u642D\u5EFA\u4F60\u7684\u7B2C\u4E00\u4E2A\u535A\u5BA2
37
- - [\u5982\u4F55\u6DFB\u52A0\u65B0\u6587\u7AE0](/zh/posts/adding-new-post) \u2014 \u5185\u5BB9\u521B\u4F5C\u6307\u5357
38
- - [\u9884\u5B9A\u4E49\u914D\u8272\u65B9\u6848](/zh/posts/predefined-color-schemes) \u2014 \u9009\u4E00\u4E2A\u4F60\u559C\u6B22\u7684\u4E3B\u9898\u8272
39
-
40
- **\u6280\u672F\u6DF1\u5EA6\uFF1A**
41
- - [\u5982\u4F55\u5728\u535A\u5BA2\u4E2D\u4F7F\u7528 LaTeX \u516C\u5F0F](/zh/posts/how-to-add-latex-equations-in-blog-posts) \u2014 \u6570\u5B66\u516C\u5F0F\u652F\u6301
42
- - [\u52A8\u6001 OG \u56FE\u7247\u751F\u6210](/zh/posts/dynamic-og-images) \u2014 \u81EA\u52A8\u751F\u6210\u793E\u4EA4\u5206\u4EAB\u56FE
43
-
44
- \u4F60\u5BF9\u54EA\u4E2A\u65B9\u5411\u7684\u5185\u5BB9\u66F4\u611F\u5174\u8DA3\uFF1F\u6211\u53EF\u4EE5\u505A\u66F4\u7CBE\u51C6\u7684\u63A8\u8350\u3002`,
45
- en: `Here are some recommended articles:
46
-
47
- **Getting Started:**
48
- - [Getting Started: Two Integration Methods](/en/posts/getting-started) \u2014 Build your first blog
49
- - [Adding New Posts](/en/posts/adding-new-post) \u2014 Content creation guide
50
- - [Predefined Color Schemes](/en/posts/predefined-color-schemes) \u2014 Pick your favorite theme color
51
-
52
- **Technical Deep Dives:**
53
- - [LaTeX Equations in Blog Posts](/en/posts/how-to-add-latex-equations-in-blog-posts) \u2014 Math formula support
54
- - [Dynamic OG Images](/en/posts/dynamic-og-images) \u2014 Auto-generate social share images
55
-
56
- What direction interests you more? I can provide more specific recommendations.`
57
- },
58
- {
59
- patterns: [/博客|blog|功能|feature/i],
60
- zh: `\u8FD9\u4E2A\u535A\u5BA2\u57FA\u4E8E **astro-minimax** \u4E3B\u9898\uFF0C\u529F\u80FD\u4E30\u5BCC\uFF1A
61
-
62
- \u6838\u5FC3\u529F\u80FD\uFF1AMarkdown/MDX\u3001\u4EE3\u7801\u9AD8\u4EAE\u3001[\u6570\u5B66\u516C\u5F0F(KaTeX)](/zh/posts/how-to-add-latex-equations-in-blog-posts)\u3001[Mermaid \u56FE\u8868](/zh/posts/mermaid-diagrams)\u3001\u6807\u7B7E\u5206\u7C7B\u3001\u5168\u6587\u641C\u7D22(Pagefind)\u3001[Waline \u8BC4\u8BBA](https://waline.js.org)\u3001\u6DF1\u8272\u6A21\u5F0F\u3002
63
-
64
- \u4E86\u89E3\u66F4\u591A\uFF1A
65
- - [\u914D\u7F6E\u6307\u5357](/zh/posts/how-to-configure-astro-minimax-theme) \u2014 \u5B8C\u6574\u914D\u7F6E\u9009\u9879
66
- - [Markdown \u6269\u5C55\u8BED\u6CD5](/zh/posts/markdown-extended) \u2014 \u6240\u6709\u652F\u6301\u7684\u8BED\u6CD5\u7279\u6027
67
-
68
- \u5F00\u6E90\u5730\u5740\uFF1A[souloss/astro-minimax](https://github.com/souloss/astro-minimax)`,
69
- en: `This blog uses the **astro-minimax** theme with rich features:
70
-
71
- Core features: Markdown/MDX, syntax highlighting, [math equations (KaTeX)](/en/posts/how-to-add-latex-equations-in-blog-posts), [Mermaid diagrams](/en/posts/mermaid-diagrams), tags & categories, full-text search (Pagefind), [Waline comments](https://waline.js.org), dark mode.
72
-
73
- Learn more:
74
- - [Configuration Guide](/en/posts/how-to-configure-astro-minimax-theme) \u2014 Full config options
75
- - [Extended Markdown](/en/posts/markdown-extended) \u2014 All supported syntax features
76
-
77
- Open source: [souloss/astro-minimax](https://github.com/souloss/astro-minimax)`
78
- },
79
- {
80
- patterns: [/主题|theme|暗色|dark|颜色|color|配色/i],
81
- zh: `\u535A\u5BA2\u652F\u6301\u4EAE\u8272\u548C\u6697\u8272\u4E3B\u9898\uFF0C\u53F3\u4E0B\u89D2\u6309\u94AE\u5373\u53EF\u5207\u6362\uFF0C\u4E5F\u4F1A\u81EA\u52A8\u68C0\u6D4B\u7CFB\u7EDF\u504F\u597D\u3002
82
-
83
- \u914D\u8272\u65B9\u6848\u53EF\u4EE5\u5728\u914D\u7F6E\u4E2D\u81EA\u5B9A\u4E49\uFF0C\u76EE\u524D\u63D0\u4F9B\u591A\u79CD\u9884\u8BBE\uFF1A
84
- - [\u9884\u5B9A\u4E49\u914D\u8272\u65B9\u6848](/zh/posts/predefined-color-schemes) \u2014 \u67E5\u770B\u6240\u6709\u53EF\u7528\u914D\u8272
85
- - [\u4E3B\u9898\u914D\u7F6E\u6307\u5357](/zh/posts/how-to-configure-astro-minimax-theme) \u2014 \u521B\u5EFA\u4F60\u81EA\u5DF1\u7684\u914D\u8272
86
-
87
- \u53C2\u8003 [Tailwind CSS \u8C03\u8272\u677F](https://tailwindcss.com/docs/customizing-colors) \u83B7\u53D6\u7075\u611F\u3002`,
88
- en: `The blog supports light and dark themes \u2014 toggle with the bottom-right button or auto-detect system preference.
89
-
90
- Color schemes are customizable:
91
- - [Predefined Color Schemes](/en/posts/predefined-color-schemes) \u2014 See all available schemes
92
- - [Theme Configuration Guide](/en/posts/how-to-configure-astro-minimax-theme) \u2014 Create your own
93
-
94
- Check [Tailwind CSS Color Palette](https://tailwindcss.com/docs/customizing-colors) for inspiration.`
95
- },
96
- {
97
- patterns: [/搭建|部署|deploy|build|install|安装|搭/i],
98
- zh: `\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\u975E\u5E38\u7B80\u5355\uFF01\u6709\u4E24\u79CD\u65B9\u5F0F\uFF1A
99
-
100
- 1. **GitHub \u6A21\u677F**\uFF08\u63A8\u8350\u65B0\u624B\uFF09\u2014 \u4E00\u952E Fork\uFF0C\u5F00\u7BB1\u5373\u7528
101
- 2. **NPM \u5305\u96C6\u6210** \u2014 \u9002\u5408\u5185\u5BB9\u4E0E\u7CFB\u7EDF\u5206\u79BB\u7684\u8FDB\u9636\u7528\u6CD5
102
-
103
- \u8BE6\u7EC6\u6B65\u9AA4\u8BF7\u770B [\u5FEB\u901F\u4E0A\u624B](/zh/posts/getting-started)\u3002
104
-
105
- \u90E8\u7F72\u63A8\u8350 [Cloudflare Pages](https://pages.cloudflare.com)\uFF08\u514D\u8D39\u3001\u5168\u7403 CDN\uFF09\uFF0C\u4E5F\u652F\u6301 [Vercel](https://vercel.com) \u548C [Netlify](https://netlify.com)\u3002`,
106
- en: `Setting up a similar blog is easy! Two methods:
107
-
108
- 1. **GitHub Template** (recommended for beginners) \u2014 One-click fork, ready to use
109
- 2. **NPM Package Integration** \u2014 For advanced content/system separation
110
-
111
- See [Getting Started](/en/posts/getting-started) for detailed steps.
112
-
113
- Deploy with [Cloudflare Pages](https://pages.cloudflare.com) (free, global CDN), or [Vercel](https://vercel.com) / [Netlify](https://netlify.com).`
114
- },
115
- {
116
- patterns: [/rust/i],
117
- zh: `\u535A\u5BA2\u4E2D\u6709\u4E00\u7CFB\u5217 Rust \u6587\u7AE0\uFF1A
118
- - [Rust \u5165\u95E8\u4ECB\u7ECD](/zh/posts/rust-series-01-introduction) \u2014 \u8BED\u8A00\u57FA\u7840
119
- - [\u6240\u6709\u6743\u7CFB\u7EDF](/zh/posts/rust-series-02-ownership) \u2014 Rust \u6838\u5FC3\u6982\u5FF5
120
- - [\u9519\u8BEF\u5904\u7406](/zh/posts/rust-series-03-error-handling) \u2014 Result \u548C Option
121
- - [\u5E76\u53D1\u7F16\u7A0B](/zh/posts/rust-series-04-concurrency) \u2014 \u5B89\u5168\u7684\u591A\u7EBF\u7A0B
122
-
123
- \u5916\u90E8\u5B66\u4E60\u8D44\u6E90\uFF1A
124
- - [The Rust Book](https://doc.rust-lang.org/book/) \u2014 \u5B98\u65B9\u6559\u7A0B
125
- - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) \u2014 \u5B9E\u4F8B\u5B66\u4E60`,
126
- en: `The blog has a Rust series:
127
- - [Rust Introduction](/en/posts/rust-series-01-introduction) \u2014 Language basics
128
- - [Ownership System](/en/posts/rust-series-02-ownership) \u2014 Core Rust concept
129
- - [Error Handling](/en/posts/rust-series-03-error-handling) \u2014 Result and Option
130
- - [Concurrency](/en/posts/rust-series-04-concurrency) \u2014 Safe multithreading
131
-
132
- External resources:
133
- - [The Rust Book](https://doc.rust-lang.org/book/) \u2014 Official tutorial
134
- - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) \u2014 Learn by examples`
135
- },
136
- {
137
- patterns: [/ai|人工智能|助手|assistant|chat/i],
138
- zh: `\u6211\u662F\u8FD9\u4E2A\u535A\u5BA2\u7684 AI \u52A9\u624B\uFF01\u5F53\u524D\u8FD0\u884C\u5728 Demo \u6A21\u5F0F\uFF0C\u53EF\u4EE5\uFF1A
139
- - \u6839\u636E\u4F60\u7684\u95EE\u9898\u63A8\u8350\u76F8\u5173\u535A\u5BA2\u6587\u7AE0
140
- - \u63A8\u8350\u6709\u7528\u7684\u5916\u90E8\u5B66\u4E60\u8D44\u6E90
141
- - \u89E3\u7B54\u5173\u4E8E\u535A\u5BA2\u6280\u672F\u6808\u7684\u95EE\u9898
142
-
143
- \u542F\u7528\u5B8C\u6574 AI \u529F\u80FD\uFF08RAG \u641C\u7D22\u589E\u5F3A\uFF09\u9700\u8981\u914D\u7F6E \`AI_BASE_URL\` \u548C \`AI_API_KEY\` \u73AF\u5883\u53D8\u91CF\u3002
144
-
145
- \u8BD5\u8BD5\u95EE\u6211\uFF1A"\u6709\u54EA\u4E9B\u6587\u7AE0\u63A8\u8350\uFF1F" \u6216 "\u600E\u4E48\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\uFF1F"`,
146
- en: `I'm the blog AI assistant! Currently in Demo mode, I can:
147
- - Recommend relevant blog articles based on your questions
148
- - Suggest useful external learning resources
149
- - Answer questions about the blog's tech stack
150
-
151
- For full AI features (RAG search enhancement), configure \`AI_BASE_URL\` and \`AI_API_KEY\` environment variables.
152
-
153
- Try asking: "Recommend some articles?" or "How to build a similar blog?"`
154
- },
155
- {
156
- patterns: [/搜索|search|pagefind/i],
157
- zh: `\u535A\u5BA2\u96C6\u6210\u4E86 [Pagefind](https://pagefind.app) \u5168\u6587\u641C\u7D22\u5F15\u64CE\uFF0C\u6784\u5EFA\u65F6\u81EA\u52A8\u7D22\u5F15\u3002\u70B9\u51FB\u9875\u9762\u9876\u90E8\u641C\u7D22\u56FE\u6807\u5373\u53EF\u4F7F\u7528\u3002
158
-
159
- \u4E86\u89E3\u66F4\u591A\u641C\u7D22\u529F\u80FD\uFF1A
160
- - [Pagefind \u5B98\u65B9\u6587\u6863](https://pagefind.app/docs/) \u2014 \u5B8C\u6574\u914D\u7F6E\u6307\u5357
161
- - \u641C\u7D22\u652F\u6301\u4E2D\u6587\u548C\u82F1\u6587\u5185\u5BB9`,
162
- en: `The blog integrates [Pagefind](https://pagefind.app) for full-text search, auto-indexed at build time. Click the search icon at the top to use it.
163
-
164
- Learn more:
165
- - [Pagefind Documentation](https://pagefind.app/docs/) \u2014 Complete configuration guide
166
- - Search supports both Chinese and English content`
167
- },
168
- {
169
- patterns: [/markdown|mdx|语法|syntax|公式|latex|mermaid|图表/i],
170
- zh: `\u535A\u5BA2\u652F\u6301\u4E30\u5BCC\u7684\u5185\u5BB9\u8BED\u6CD5\uFF1A
171
-
172
- - [Markdown \u57FA\u7840\u8BED\u6CD5](/zh/posts/markdown-basics) \u2014 \u6807\u9898\u3001\u5217\u8868\u3001\u8868\u683C\u7B49
173
- - [Markdown \u6269\u5C55\u8BED\u6CD5](/zh/posts/markdown-extended) \u2014 \u811A\u6CE8\u3001\u9AD8\u4EAE\u3001\u6298\u53E0\u7B49
174
- - [LaTeX \u6570\u5B66\u516C\u5F0F](/zh/posts/how-to-add-latex-equations-in-blog-posts) \u2014 KaTeX \u6E32\u67D3
175
- - [Mermaid \u56FE\u8868](/zh/posts/mermaid-diagrams) \u2014 \u6D41\u7A0B\u56FE\u3001\u65F6\u5E8F\u56FE
176
- - [Markmap \u601D\u7EF4\u5BFC\u56FE](/zh/posts/markmap-mindmaps) \u2014 \u4EA4\u4E92\u5F0F\u601D\u7EF4\u5BFC\u56FE
177
-
178
- \u5916\u90E8\u53C2\u8003\uFF1A[GitHub Flavored Markdown](https://github.github.com/gfm/)`,
179
- en: `The blog supports rich content syntax:
180
-
181
- - [Markdown Basics](/en/posts/markdown-basics) \u2014 Headings, lists, tables
182
- - [Extended Markdown](/en/posts/markdown-extended) \u2014 Footnotes, highlights, collapsible
183
- - [LaTeX Equations](/en/posts/how-to-add-latex-equations-in-blog-posts) \u2014 KaTeX rendering
184
- - [Mermaid Diagrams](/en/posts/mermaid-diagrams) \u2014 Flowcharts, sequence diagrams
185
- - [Markmap Mind Maps](/en/posts/markmap-mindmaps) \u2014 Interactive mind maps
186
-
187
- Reference: [GitHub Flavored Markdown](https://github.github.com/gfm/)`
188
- }
189
- ];
190
- var FALLBACK = {
191
- zh: `\u611F\u8C22\u63D0\u95EE\uFF01\u6211\u76EE\u524D\u5728 Demo \u6A21\u5F0F\u4E0B\uFF0C\u53EF\u4EE5\u63A8\u8350\u535A\u5BA2\u6587\u7AE0\u548C\u5916\u90E8\u8D44\u6E90\u3002
192
-
193
- \u8BD5\u8BD5\u8FD9\u4E9B\u8BDD\u9898\uFF1A
194
- - "\u6709\u54EA\u4E9B\u6587\u7AE0\u63A8\u8350\uFF1F"
195
- - "Astro \u6846\u67B6\u662F\u4EC0\u4E48\uFF1F"
196
- - "\u600E\u4E48\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\uFF1F"
197
- - "\u652F\u6301\u54EA\u4E9B Markdown \u8BED\u6CD5\uFF1F"`,
198
- en: `Thanks for asking! I'm in Demo mode and can recommend blog articles and external resources.
199
-
200
- Try these topics:
201
- - "Recommend some articles?"
202
- - "What is Astro?"
203
- - "How to build a similar blog?"
204
- - "What Markdown syntax is supported?"`
205
- };
206
- function getMockResponse(question, lang = "zh") {
207
- const q = question.toLowerCase();
208
- const isZh = lang !== "en";
209
- for (const { patterns, zh, en } of MOCK_RESPONSES) {
210
- if (patterns.some((p) => p.test(q))) {
211
- return isZh ? zh : en;
212
- }
6
+ import { APICallError } from "@ai-sdk/provider";
7
+ import { getMockResponse, createMockStream } from "../providers/mock.js";
8
+ import { t, getLang } from "../utils/i18n.js";
9
+ import { shouldAutoContinueAfterToolCalls } from "./tool-auto-continue.js";
10
+ import { RichText } from "./RichText.js";
11
+ import { ReasoningBlock } from "./ReasoningBlock.js";
12
+ import { AssistantMessage, BotAvatar, BotIcon, TypingDots, getTextFromMessage } from "./MessageBubble.js";
13
+ import { ChatInput, resetInputHeight } from "./ChatInput.js";
14
+ const MIN_SEND_INTERVAL_MS = 500;
15
+ let lastChatTrigger = null;
16
+ function getChatActionLabel(lang, type) {
17
+ const normalizedLang = getLang(lang);
18
+ if (type === "sending") {
19
+ return normalizedLang === "zh" ? "\u6B63\u5728\u53D1\u9001" : "Sending";
213
20
  }
214
- return isZh ? FALLBACK.zh : FALLBACK.en;
215
- }
216
- function createMockStream(text) {
217
- let index = 0;
218
- return new ReadableStream({
219
- async pull(controller) {
220
- if (index >= text.length) {
221
- controller.close();
222
- return;
223
- }
224
- const chunkSize = Math.random() < 0.3 ? 2 : 1;
225
- const chunk = text.slice(index, index + chunkSize);
226
- index += chunkSize;
227
- controller.enqueue(chunk);
228
- await new Promise((resolve) => setTimeout(resolve, 12 + Math.random() * 23));
229
- }
230
- });
231
- }
232
-
233
- // src/server/types.ts
234
- function isChatStatusData(value) {
235
- if (!value || typeof value !== "object") return false;
236
- const v = value;
237
- return typeof v.stage === "string" && typeof v.message === "string" && typeof v.progress === "number";
238
- }
239
-
240
- // src/utils/i18n.ts
241
- var translations = {
242
- en: {
243
- // Reasoning UI
244
- "ai.reasoning.thinking": "Thinking...",
245
- "ai.reasoning.viewReasoning": "View reasoning",
246
- "ai.reasoning.waiting": "Waiting for thoughts...",
247
- // Error messages
248
- "ai.error.network": "Network connection failed. Please check your connection.",
249
- "ai.error.aborted": "Request was cancelled.",
250
- "ai.error.rateLimit": "Too many requests. Please try again later.",
251
- "ai.error.unavailable": "AI service is temporarily unavailable.",
252
- "ai.error.generic": "Something went wrong. Please try again later.",
253
- "ai.error.format": "Invalid request format.",
254
- // UI labels
255
- "ai.placeholder": "Ask a question...",
256
- "ai.clear": "Clear",
257
- "ai.clearConversation": "Clear conversation",
258
- "ai.close": "Close",
259
- "ai.closeChat": "Close chat",
260
- "ai.retry": "Retry",
261
- "ai.status.searching": "Searching...",
262
- "ai.status.generating": "Generating response...",
263
- "ai.status.found": "Found {count} related items",
264
- "ai.status.citation": "Answered from public records",
265
- "ai.status.fallback": "AI service unavailable, using demo mode",
266
- // Quick prompts
267
- "ai.prompt.techStack": "What tech stack is used?",
268
- "ai.prompt.recommend": "Recommend some articles?",
269
- "ai.prompt.build": "How to build a similar blog?",
270
- "ai.prompt.summarize": 'Summarize the key points of "{title}"',
271
- "ai.prompt.explain": 'Explain "{point}"',
272
- "ai.prompt.related": "What related content should I read next?",
273
- // Welcome messages
274
- "ai.welcome.reading": `I'm reading "{title}" with you.
275
- Ask me to summarize, explain a concept, or explore related topics.`,
276
- "ai.welcome.canHelp": "Hi! I'm the blog AI assistant. Ask me anything and I'll help you find related articles.",
277
- "ai.welcome.greeting": "Hi! I'm the blog AI assistant.",
278
- "ai.welcome.demo": "I'm running in demo mode. I can recommend blog articles and external resources.",
279
- "ai.welcome.demoHint": "For full AI features (RAG search), configure AI_BASE_URL and AI_API_KEY.",
280
- "ai.welcome.demoPrompt": 'Try: "Recommend articles?" or "How to build this blog?"',
281
- // Header
282
- "ai.header.reading": "Reading:",
283
- "ai.header.mode": "Demo",
284
- // Assistant branding
285
- "ai.assistantName": "Blog Avatar",
286
- "ai.status.live": "Live",
287
- // Additional error messages
288
- "ai.error.emptyMessage": "Message cannot be empty.",
289
- "ai.error.emptyContent": "Message content cannot be empty.",
290
- "ai.error.inputTooLong": "Message too long, max {max} characters.",
291
- "ai.error.timeout": "Response timeout, please retry or simplify your question.",
292
- // Rate limit messages
293
- "ai.error.rateLimit.burst": "Too many requests, please try again later.",
294
- "ai.error.rateLimit.sustained": "Too many requests, please wait a minute.",
295
- "ai.error.rateLimit.daily": "Daily limit reached, please come back tomorrow.",
296
- "ai.error.noOutput": "Sorry, I could not generate a valid response. Please try rephrasing your question.",
297
- "ai.prompt.section.responsibilities": "Your Responsibilities",
298
- "ai.prompt.section.format": "Response Format",
299
- "ai.prompt.section.principles": "Recommendation Principles",
300
- "ai.prompt.section.constraints": "Constraints",
301
- "ai.prompt.section.sourceLayers": "Source Priority Protocol (must follow)",
302
- "ai.prompt.section.privacy": "Privacy Protection",
303
- "ai.prompt.section.answerModes": "Answer Mode Guide (follow detected mode)",
304
- "ai.prompt.section.preOutputChecks": "Pre-Output Checks (execute mentally, do not output steps)",
305
- "ai.semiStatic.blogOverview": "Blog Overview",
306
- "ai.semiStatic.totalPosts": "{count} posts total",
307
- "ai.semiStatic.mainCategories": "Main categories: {categories}",
308
- "ai.semiStatic.latestArticles": "Latest Posts"
309
- },
310
- zh: {
311
- // Reasoning UI
312
- "ai.reasoning.thinking": "\u601D\u8003\u4E2D...",
313
- "ai.reasoning.viewReasoning": "\u67E5\u770B\u601D\u8003\u8FC7\u7A0B",
314
- "ai.reasoning.waiting": "\u7B49\u5F85\u601D\u8003...",
315
- // Error messages
316
- "ai.error.network": "\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC",
317
- "ai.error.aborted": "\u8BF7\u6C42\u5DF2\u53D6\u6D88",
318
- "ai.error.rateLimit": "\u8BF7\u6C42\u592A\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
319
- "ai.error.unavailable": "AI \u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528",
320
- "ai.error.generic": "\u51FA\u4E86\u70B9\u95EE\u9898\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
321
- "ai.error.format": "\u8BF7\u6C42\u683C\u5F0F\u9519\u8BEF",
322
- // UI labels
323
- "ai.placeholder": "\u8F93\u5165\u4F60\u7684\u95EE\u9898...",
324
- "ai.clear": "\u6E05\u9664",
325
- "ai.clearConversation": "\u6E05\u9664\u5BF9\u8BDD",
326
- "ai.close": "\u5173\u95ED",
327
- "ai.closeChat": "\u5173\u95ED\u804A\u5929",
328
- "ai.retry": "\u91CD\u8BD5",
329
- "ai.status.searching": "\u641C\u7D22\u4E2D...",
330
- "ai.status.generating": "\u6B63\u5728\u751F\u6210\u56DE\u7B54...",
331
- "ai.status.found": "\u627E\u5230 {count} \u7BC7\u76F8\u5173\u5185\u5BB9",
332
- "ai.status.citation": "\u5DF2\u57FA\u4E8E\u516C\u5F00\u8BB0\u5F55\u76F4\u63A5\u7ED9\u51FA\u56DE\u7B54",
333
- "ai.status.fallback": "AI \u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u4F7F\u7528\u6F14\u793A\u6A21\u5F0F\u56DE\u590D",
334
- // Quick prompts
335
- "ai.prompt.techStack": "\u8FD9\u4E2A\u535A\u5BA2\u7528\u4E86\u4EC0\u4E48\u6280\u672F\uFF1F",
336
- "ai.prompt.recommend": "\u6709\u54EA\u4E9B\u6587\u7AE0\u63A8\u8350\uFF1F",
337
- "ai.prompt.build": "\u600E\u4E48\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\uFF1F",
338
- "ai.prompt.summarize": "\u603B\u7ED3\u4E00\u4E0B\u300A{title}\u300B\u7684\u6838\u5FC3\u89C2\u70B9",
339
- "ai.prompt.explain": "\u89E3\u91CA\u4E00\u4E0B\u300C{point}\u300D",
340
- "ai.prompt.related": "\u8FD9\u7BC7\u6587\u7AE0\u548C\u54EA\u4E9B\u5185\u5BB9\u76F8\u5173\uFF1F",
341
- // Welcome messages
342
- "ai.welcome.reading": "\u6211\u5728\u7ED3\u5408\u300A{title}\u300B\u966A\u4F60\u9605\u8BFB\u3002\n\u4F60\u53EF\u4EE5\u8BA9\u6211\u603B\u7ED3\u8FD9\u7BC7\u6587\u7AE0\u3001\u89E3\u91CA\u67D0\u4E2A\u89C2\u70B9\uFF0C\u6216\u8005\u987A\u7740\u8FD9\u7BC7\u6587\u7AE0\u7EE7\u7EED\u5EF6\u4F38\u5230\u76F8\u5173\u4E3B\u9898\u3002",
343
- "ai.welcome.canHelp": "\u4F60\u597D\uFF01\u6211\u662F\u535A\u5BA2 AI \u52A9\u624B\uFF0C\u95EE\u6211\u4EFB\u4F55\u5173\u4E8E\u535A\u5BA2\u5185\u5BB9\u7684\u95EE\u9898\uFF0C\u6211\u53EF\u4EE5\u5E2E\u4F60\u627E\u5230\u76F8\u5173\u6587\u7AE0\u3002",
344
- "ai.welcome.greeting": "\u4F60\u597D\uFF01\u6211\u662F\u535A\u5BA2 AI \u52A9\u624B\u3002",
345
- "ai.welcome.demo": "\u6211\u76EE\u524D\u5728 Demo \u6A21\u5F0F\u4E0B\uFF0C\u53EF\u4EE5\u63A8\u8350\u535A\u5BA2\u6587\u7AE0\u548C\u5916\u90E8\u8D44\u6E90\u3002",
346
- "ai.welcome.demoHint": "\u542F\u7528\u5B8C\u6574 AI \u529F\u80FD\uFF08RAG \u641C\u7D22\u589E\u5F3A\uFF09\u9700\u8981\u914D\u7F6E AI_BASE_URL \u548C AI_API_KEY \u73AF\u5883\u53D8\u91CF\u3002",
347
- "ai.welcome.demoPrompt": "\u8BD5\u8BD5\uFF1A\u300C\u6709\u54EA\u4E9B\u6587\u7AE0\u63A8\u8350\uFF1F\u300D\u6216\u300C\u600E\u4E48\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\uFF1F\u300D",
348
- // Header
349
- "ai.header.reading": "\u6B63\u5728\u9605\u8BFB\uFF1A",
350
- "ai.header.mode": "\u6F14\u793A",
351
- // Assistant branding
352
- "ai.assistantName": "\u535A\u5BA2\u5206\u8EAB",
353
- "ai.status.live": "\u5728\u7EBF",
354
- // Additional error messages
355
- "ai.error.emptyMessage": "\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A\u3002",
356
- "ai.error.emptyContent": "\u6D88\u606F\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A\u3002",
357
- "ai.error.inputTooLong": "\u6D88\u606F\u8FC7\u957F\uFF0C\u6700\u591A {max} \u5B57\u3002",
358
- "ai.error.timeout": "\u54CD\u5E94\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5\u6216\u7B80\u5316\u95EE\u9898\u3002",
359
- // Rate limit messages
360
- "ai.error.rateLimit.burst": "\u8BF7\u6C42\u592A\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
361
- "ai.error.rateLimit.sustained": "\u8BF7\u6C42\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u4E00\u5206\u949F\u540E\u518D\u8BD5\u3002",
362
- "ai.error.rateLimit.daily": "\u4ECA\u65E5\u5BF9\u8BDD\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u660E\u5929\u518D\u6765\u3002",
363
- "ai.error.noOutput": "\u62B1\u6B49\uFF0C\u6211\u65E0\u6CD5\u751F\u6210\u6709\u6548\u7684\u56DE\u7B54\u3002\u8BF7\u5C1D\u8BD5\u6362\u4E00\u79CD\u65B9\u5F0F\u63D0\u95EE\u3002",
364
- "ai.prompt.section.responsibilities": "\u4F60\u7684\u804C\u8D23",
365
- "ai.prompt.section.format": "\u56DE\u7B54\u683C\u5F0F",
366
- "ai.prompt.section.principles": "\u63A8\u8350\u539F\u5219",
367
- "ai.prompt.section.constraints": "\u7EA6\u675F",
368
- "ai.prompt.section.sourceLayers": "\u6765\u6E90\u5206\u5C42\u534F\u8BAE\uFF08\u5FC5\u987B\u9075\u5B88\uFF09",
369
- "ai.prompt.section.privacy": "\u9690\u79C1\u4FDD\u62A4",
370
- "ai.prompt.section.answerModes": "\u56DE\u7B54\u6A21\u5F0F\u6307\u5BFC\uFF08\u6309\u68C0\u6D4B\u5230\u7684\u6A21\u5F0F\u6267\u884C\uFF09",
371
- "ai.prompt.section.preOutputChecks": "\u8F93\u51FA\u524D\u68C0\u67E5\uFF08\u5728\u5FC3\u91CC\u6267\u884C\uFF0C\u4E0D\u8F93\u51FA\u6B65\u9AA4\uFF09",
372
- "ai.semiStatic.blogOverview": "\u535A\u5BA2\u6982\u51B5",
373
- "ai.semiStatic.totalPosts": "\u5171\u6709 {count} \u7BC7\u6587\u7AE0",
374
- "ai.semiStatic.mainCategories": "\u4E3B\u8981\u5206\u7C7B\uFF1A{categories}",
375
- "ai.semiStatic.latestArticles": "\u6700\u65B0\u6587\u7AE0"
376
- }
377
- };
378
- function t(key, lang = "zh", vars) {
379
- const l = lang === "zh" ? "zh" : "en";
380
- let text = translations[l]?.[key] ?? translations["en"][key] ?? key;
381
- if (vars) {
382
- for (const [k, v] of Object.entries(vars)) {
383
- text = text.replace(new RegExp(`\\{${k}\\}`, "g"), String(v));
384
- }
385
- }
386
- return text;
387
- }
388
- function getLang(lang) {
389
- return lang === "zh" ? "zh" : "en";
390
- }
391
-
392
- // src/components/ChatPanel.tsx
393
- var MIN_SEND_INTERVAL_MS = 500;
394
- var TYPEWRITER_SPEED_MS = 25;
395
- var TYPEWRITER_BATCH_SIZE = 1;
396
- function useTypewriter(fullText, isStreaming) {
397
- const [displayedLength, setDisplayedLength] = useState(0);
398
- const prevFullTextRef = useRef(fullText);
399
- const prevStreamingRef = useRef(isStreaming);
400
- const animationRef = useRef(null);
401
- useEffect(() => {
402
- if (fullText !== prevFullTextRef.current && !fullText.startsWith(prevFullTextRef.current)) {
403
- setDisplayedLength(0);
404
- }
405
- prevFullTextRef.current = fullText;
406
- }, [fullText]);
407
- useEffect(() => {
408
- if (!isStreaming && prevStreamingRef.current) {
409
- setDisplayedLength(fullText.length);
410
- if (animationRef.current) {
411
- cancelAnimationFrame(animationRef.current);
412
- animationRef.current = null;
413
- }
414
- }
415
- prevStreamingRef.current = isStreaming;
416
- }, [isStreaming, fullText.length]);
417
- useEffect(() => {
418
- if (!isStreaming) return;
419
- let lastTime = performance.now();
420
- const animate = (currentTime) => {
421
- const elapsed = currentTime - lastTime;
422
- if (elapsed >= TYPEWRITER_SPEED_MS) {
423
- setDisplayedLength((prev) => {
424
- const targetLength = fullText.length;
425
- if (prev >= targetLength) return prev;
426
- const behind = targetLength - prev;
427
- const speed = behind > 20 ? Math.min(behind, 5) : TYPEWRITER_BATCH_SIZE;
428
- return Math.min(prev + speed, targetLength);
429
- });
430
- lastTime = currentTime;
431
- }
432
- animationRef.current = requestAnimationFrame(animate);
433
- };
434
- animationRef.current = requestAnimationFrame(animate);
435
- return () => {
436
- if (animationRef.current) {
437
- cancelAnimationFrame(animationRef.current);
438
- animationRef.current = null;
439
- }
440
- };
441
- }, [isStreaming, fullText]);
442
- return fullText.slice(0, displayedLength) || (isStreaming ? "" : fullText);
21
+ return normalizedLang === "zh" ? "\u53D1\u9001\u6D88\u606F" : "Send message";
443
22
  }
444
23
  function generateSessionId(articleContext) {
445
24
  if (articleContext?.slug) return `article:${articleContext.slug}`;
446
25
  if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
447
26
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
448
27
  }
449
- function getTextFromMessage(message) {
450
- const parts = message.parts ?? [];
451
- return Array.isArray(parts) ? parts.filter((p) => p.type === "text").map((p) => p.text).join("") : "";
452
- }
453
- var QUICK_PROMPTS_ZH = [
28
+ const QUICK_PROMPTS_ZH = [
454
29
  t("ai.prompt.techStack", "zh"),
455
30
  t("ai.prompt.recommend", "zh"),
456
31
  t("ai.prompt.build", "zh")
457
32
  ];
458
- var QUICK_PROMPTS_EN = [
33
+ const QUICK_PROMPTS_EN = [
459
34
  t("ai.prompt.techStack", "en"),
460
35
  t("ai.prompt.recommend", "en"),
461
36
  t("ai.prompt.build", "en")
@@ -494,6 +69,18 @@ function buildWelcomeMessage(config, articleContext) {
494
69
  }
495
70
  function parseErrorMessage(error, lang = "zh") {
496
71
  const l = getLang(lang);
72
+ if (APICallError.isInstance(error)) {
73
+ if (error.responseBody) {
74
+ try {
75
+ const parsed = JSON.parse(error.responseBody);
76
+ if (parsed?.error) return parsed.error;
77
+ } catch {
78
+ }
79
+ }
80
+ if (error.data && typeof error.data === "object" && "error" in error.data) {
81
+ return String(error.data.error);
82
+ }
83
+ }
497
84
  try {
498
85
  const parsed = JSON.parse(error.message);
499
86
  if (parsed?.error) return parsed.error;
@@ -507,6 +94,9 @@ function parseErrorMessage(error, lang = "zh") {
507
94
  return t("ai.error.generic", l);
508
95
  }
509
96
  function isRetryable(error) {
97
+ if (APICallError.isInstance(error)) {
98
+ return error.isRetryable;
99
+ }
510
100
  try {
511
101
  const parsed = JSON.parse(error.message);
512
102
  if (typeof parsed?.retryable === "boolean") return parsed.retryable;
@@ -514,171 +104,6 @@ function isRetryable(error) {
514
104
  }
515
105
  return true;
516
106
  }
517
- function parseInlineMarkdown(text) {
518
- const parts = [];
519
- const re = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|`([^`]+)`/g;
520
- let lastIndex = 0;
521
- let match;
522
- while ((match = re.exec(text)) !== null) {
523
- if (match.index > lastIndex) {
524
- parts.push({ type: "text", text: text.slice(lastIndex, match.index) });
525
- }
526
- if (match[1] && match[2]) parts.push({ type: "link", label: match[1], url: match[2] });
527
- else if (match[3]) parts.push({ type: "bold", text: match[3] });
528
- else if (match[4]) parts.push({ type: "code", text: match[4] });
529
- lastIndex = match.index + match[0].length;
530
- }
531
- if (lastIndex < text.length) parts.push({ type: "text", text: text.slice(lastIndex) });
532
- return parts;
533
- }
534
- function InlineRichText({ text }) {
535
- const parts = useMemo(() => parseInlineMarkdown(text), [text]);
536
- return /* @__PURE__ */ h("span", null, parts.map((p, i) => {
537
- if (p.type === "link") {
538
- const isExternal = p.url.startsWith("http");
539
- return /* @__PURE__ */ h(
540
- "a",
541
- {
542
- key: i,
543
- href: p.url,
544
- class: "inline-flex items-center gap-0.5 font-medium text-accent underline decoration-accent/30 underline-offset-2 transition-colors hover:decoration-accent",
545
- target: isExternal ? "_blank" : void 0,
546
- rel: isExternal ? "noopener noreferrer" : void 0
547
- },
548
- p.label,
549
- isExternal && /* @__PURE__ */ h(ExternalLinkIcon, null)
550
- );
551
- }
552
- if (p.type === "bold") return /* @__PURE__ */ h("strong", { key: i, class: "font-semibold" }, p.text);
553
- if (p.type === "code") return /* @__PURE__ */ h("code", { key: i, class: "rounded bg-muted/60 px-1 py-0.5 text-[13px] font-mono" }, p.text);
554
- return /* @__PURE__ */ h("span", { key: i }, p.text);
555
- }));
556
- }
557
- function parseBlocks(text) {
558
- const lines = text.split("\n");
559
- const blocks = [];
560
- let i = 0;
561
- while (i < lines.length) {
562
- const line = lines[i];
563
- if (line.startsWith("```")) {
564
- const lang = line.slice(3).trim();
565
- const codeLines = [];
566
- i++;
567
- while (i < lines.length && !lines[i].startsWith("```")) {
568
- codeLines.push(lines[i]);
569
- i++;
570
- }
571
- if (i < lines.length) i++;
572
- blocks.push({ type: "code-block", content: codeLines.join("\n"), lang: lang || void 0 });
573
- continue;
574
- }
575
- if (line.startsWith("> ") || line === ">") {
576
- const quoteLines = [];
577
- while (i < lines.length && (lines[i].startsWith("> ") || lines[i] === ">")) {
578
- quoteLines.push(lines[i].replace(/^>\s?/, ""));
579
- i++;
580
- }
581
- blocks.push({ type: "blockquote", content: quoteLines.join("\n") });
582
- continue;
583
- }
584
- if (/^[-*]\s/.test(line)) {
585
- const items = [];
586
- while (i < lines.length && /^[-*]\s/.test(lines[i])) {
587
- items.push(lines[i].replace(/^[-*]\s/, ""));
588
- i++;
589
- }
590
- blocks.push({ type: "list", content: "", ordered: false, items });
591
- continue;
592
- }
593
- if (/^\d+\.\s/.test(line)) {
594
- const items = [];
595
- while (i < lines.length && /^\d+\.\s/.test(lines[i])) {
596
- items.push(lines[i].replace(/^\d+\.\s/, ""));
597
- i++;
598
- }
599
- blocks.push({ type: "list", content: "", ordered: true, items });
600
- continue;
601
- }
602
- if (!line.trim()) {
603
- i++;
604
- continue;
605
- }
606
- const paraLines = [];
607
- while (i < lines.length && lines[i].trim() && !lines[i].startsWith("```") && !lines[i].startsWith("> ") && lines[i] !== ">" && !/^[-*]\s/.test(lines[i]) && !/^\d+\.\s/.test(lines[i])) {
608
- paraLines.push(lines[i]);
609
- i++;
610
- }
611
- if (paraLines.length) {
612
- blocks.push({ type: "paragraph", content: paraLines.join("\n") });
613
- }
614
- }
615
- return blocks;
616
- }
617
- function RichText({ text }) {
618
- const blocks = useMemo(() => parseBlocks(text), [text]);
619
- return /* @__PURE__ */ h("div", { class: "space-y-2" }, blocks.map((block, i) => {
620
- switch (block.type) {
621
- case "code-block":
622
- return /* @__PURE__ */ h("pre", { key: i, class: "overflow-x-auto rounded-md bg-muted/60 px-3 py-2 text-[12px] leading-relaxed font-mono" }, /* @__PURE__ */ h("code", null, block.content));
623
- case "blockquote":
624
- return /* @__PURE__ */ h("blockquote", { key: i, class: "border-l-2 border-accent/40 pl-3 text-foreground-soft italic" }, /* @__PURE__ */ h(InlineRichText, { text: block.content }));
625
- case "list":
626
- if (block.ordered) {
627
- return /* @__PURE__ */ h("ol", { key: i, class: "list-decimal space-y-0.5 pl-5" }, block.items?.map((item, j) => /* @__PURE__ */ h("li", { key: j }, /* @__PURE__ */ h(InlineRichText, { text: item }))));
628
- }
629
- return /* @__PURE__ */ h("ul", { key: i, class: "list-disc space-y-0.5 pl-5" }, block.items?.map((item, j) => /* @__PURE__ */ h("li", { key: j }, /* @__PURE__ */ h(InlineRichText, { text: item }))));
630
- case "paragraph":
631
- default:
632
- return /* @__PURE__ */ h("p", { key: i, class: "whitespace-pre-wrap" }, /* @__PURE__ */ h(InlineRichText, { text: block.content }));
633
- }
634
- }));
635
- }
636
- function ReasoningBlock({ text, isStreaming, lang = "zh" }) {
637
- const isEmpty = text.length === 0;
638
- const l = getLang(lang);
639
- return /* @__PURE__ */ h("details", { class: "group rounded-lg border border-border/50 bg-muted/30 overflow-hidden", open: isStreaming || !isEmpty }, /* @__PURE__ */ h("summary", { class: "flex cursor-pointer items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium text-foreground-soft transition-colors hover:bg-muted/50 hover:text-foreground" }, /* @__PURE__ */ h("svg", { class: "size-3.5 transition-transform group-open:rotate-90", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("path", { d: "m9 18 6-6-6-6" })), /* @__PURE__ */ h("span", { class: "flex items-center gap-1" }, isStreaming ? /* @__PURE__ */ h("svg", { class: "size-3 animate-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, /* @__PURE__ */ h("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })) : /* @__PURE__ */ h("svg", { class: "size-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ h("path", { d: "M12 16v-4" }), /* @__PURE__ */ h("path", { d: "M12 8h.01" })), isStreaming && isEmpty ? t("ai.reasoning.thinking", l) : isStreaming ? t("ai.reasoning.thinking", l).replace("...", "") : t("ai.reasoning.viewReasoning", l))), /* @__PURE__ */ h("div", { class: "border-t border-border/30 bg-background/50 px-2.5 py-2" }, isEmpty && isStreaming ? /* @__PURE__ */ h("div", { class: "flex items-center gap-2 text-[11px] text-foreground-soft" }, /* @__PURE__ */ h("span", { class: "inline-flex gap-1" }, /* @__PURE__ */ h("span", { class: "size-1.5 animate-bounce rounded-full bg-foreground-soft [animation-delay:0ms]" }), /* @__PURE__ */ h("span", { class: "size-1.5 animate-bounce rounded-full bg-foreground-soft [animation-delay:150ms]" }), /* @__PURE__ */ h("span", { class: "size-1.5 animate-bounce rounded-full bg-foreground-soft [animation-delay:300ms]" })), /* @__PURE__ */ h("span", null, t("ai.reasoning.waiting", l))) : /* @__PURE__ */ h("pre", { class: "whitespace-pre-wrap text-[11px] leading-relaxed text-foreground-soft font-mono" }, text)));
640
- }
641
- function AssistantMessage({ message, isStreaming, lang = "zh" }) {
642
- const fullText = getTextFromMessage(message);
643
- const displayedText = useTypewriter(fullText, isStreaming ?? false);
644
- const reasoningParts = message.parts.filter((p) => p.type === "reasoning");
645
- const reasoningFullText = reasoningParts.map((p) => p.text).join("");
646
- const reasoningDisplayed = useTypewriter(reasoningFullText, isStreaming ?? false);
647
- const hasReasoning = reasoningFullText.length > 0;
648
- const isWaitingForContent = isStreaming && !fullText && !reasoningFullText;
649
- const sources = message.parts.filter((p) => p.type === "source-url" || p.type === "source-document");
650
- if (isWaitingForContent) {
651
- return /* @__PURE__ */ h("div", { class: "space-y-1.5" }, /* @__PURE__ */ h(ReasoningBlock, { text: "", isStreaming: true, lang }));
652
- }
653
- if (!fullText && !hasReasoning) return null;
654
- return /* @__PURE__ */ h("div", { class: "space-y-1.5" }, hasReasoning && /* @__PURE__ */ h(ReasoningBlock, { text: reasoningDisplayed, isStreaming, lang }), displayedText && /* @__PURE__ */ h(RichText, { text: displayedText }), !isStreaming && sources.length > 0 && /* @__PURE__ */ h("div", { class: "mt-2 flex flex-wrap gap-1.5" }, sources.map((s, i) => {
655
- const part = s;
656
- return /* @__PURE__ */ h(
657
- "a",
658
- {
659
- key: i,
660
- href: part.url ?? "#",
661
- class: "inline-flex items-center gap-1 rounded-md border border-border bg-muted/30 px-2 py-0.5 text-[11px] text-foreground-soft transition-colors hover:border-accent/40 hover:text-foreground",
662
- target: "_blank",
663
- rel: "noopener noreferrer"
664
- },
665
- /* @__PURE__ */ h("svg", { class: "size-2.5 opacity-50", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, /* @__PURE__ */ h("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), /* @__PURE__ */ h("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })),
666
- part.title ?? "Source"
667
- );
668
- })));
669
- }
670
- function ExternalLinkIcon() {
671
- return /* @__PURE__ */ h("svg", { class: "inline-block size-3 shrink-0 opacity-50", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }), /* @__PURE__ */ h("polyline", { points: "15 3 21 3 21 9" }), /* @__PURE__ */ h("line", { x1: "10", y1: "14", x2: "21", y2: "3" }));
672
- }
673
- function BotAvatar() {
674
- return /* @__PURE__ */ h("div", { class: "flex size-5.5 shrink-0 items-center justify-center rounded-full bg-accent/15 mt-0.5" }, /* @__PURE__ */ h(BotIcon, { class: "size-3 text-accent" }));
675
- }
676
- function BotIcon({ class: cls }) {
677
- return /* @__PURE__ */ h("svg", { class: cls, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("path", { d: "M12 8V4H8" }), /* @__PURE__ */ h("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), /* @__PURE__ */ h("path", { d: "M2 14h2" }), /* @__PURE__ */ h("path", { d: "M20 14h2" }), /* @__PURE__ */ h("path", { d: "M15 13v2" }), /* @__PURE__ */ h("path", { d: "M9 13v2" }));
678
- }
679
- function TypingDots({ statusMessage }) {
680
- return /* @__PURE__ */ h("div", { class: "flex items-center gap-2" }, /* @__PURE__ */ h("span", { class: "inline-flex gap-1" }, /* @__PURE__ */ h("span", { class: "size-1.5 animate-bounce rounded-full bg-foreground-soft [animation-delay:0ms]" }), /* @__PURE__ */ h("span", { class: "size-1.5 animate-bounce rounded-full bg-foreground-soft [animation-delay:150ms]" }), /* @__PURE__ */ h("span", { class: "size-1.5 animate-bounce rounded-full bg-foreground-soft [animation-delay:300ms]" })), statusMessage && /* @__PURE__ */ h("span", { class: "text-[11px] text-foreground-soft" }, statusMessage));
681
- }
682
107
  function useMockChat(lang) {
683
108
  const [messages, setMessages] = useState([]);
684
109
  const [isStreaming, setIsStreaming] = useState(false);
@@ -706,20 +131,55 @@ function useMockChat(lang) {
706
131
  const clear = useCallback(() => setMessages([]), []);
707
132
  return { messages, isStreaming, sendMessage, clear };
708
133
  }
134
+ const PANEL_SIZE_CONFIG = {
135
+ S: { width: "370px", height: "min(520px, calc(100vh - 7rem))", class: "w-[370px] max-w-[calc(100vw-2rem)]" },
136
+ M: { width: "550px", height: "min(70vh, calc(100vh - 5rem))", class: "w-[550px] max-w-[calc(100vw-3rem)]" },
137
+ L: { width: "80vw", height: "80vh", class: "w-[80vw] max-w-[900px]" }
138
+ };
709
139
  function ChatPanel({ open, onClose, config, articleContext }) {
710
140
  const isMockMode = config.mockMode || !config.apiEndpoint;
711
141
  const lang = getLang(config.lang);
712
142
  const placeholder = config.placeholder ?? t("ai.placeholder", lang);
713
143
  const sessionId = useMemo(() => generateSessionId(articleContext), [articleContext]);
714
144
  const panelRef = useRef(null);
715
- const inputRef = useRef(null);
716
145
  const messagesEndRef = useRef(null);
717
146
  const lastSendRef = useRef(0);
718
147
  const [inputValue, setInputValue] = useState("");
719
148
  const [cooldown, setCooldown] = useState(false);
720
- const [statusMessage, setStatusMessage] = useState();
149
+ const titleId = articleContext ? "ai-chat-title-reading" : "ai-chat-title";
150
+ const descriptionId = articleContext ? "ai-chat-description-reading" : "ai-chat-description";
151
+ const statusId = "ai-chat-status";
152
+ const [panelSize, setPanelSize] = useState(() => {
153
+ if (typeof window === "undefined") return "S";
154
+ try {
155
+ const saved = localStorage.getItem("ai-chat-panel-size");
156
+ if (saved === "S" || saved === "M" || saved === "L") return saved;
157
+ } catch {
158
+ }
159
+ return "S";
160
+ });
721
161
  const quickPrompts = useMemo(() => getQuickPrompts(lang, articleContext), [lang, articleContext]);
722
162
  const welcomeMessage = useMemo(() => buildWelcomeMessage(config, articleContext), [config, articleContext]);
163
+ useEffect(() => {
164
+ try {
165
+ localStorage.setItem("ai-chat-panel-size", panelSize);
166
+ } catch {
167
+ }
168
+ }, [panelSize]);
169
+ useEffect(() => {
170
+ if (!open) return;
171
+ const activeElement = document.activeElement;
172
+ lastChatTrigger = activeElement instanceof HTMLElement ? activeElement : null;
173
+ const handleKeyDown = (event) => {
174
+ if (event.key === "Escape") {
175
+ event.preventDefault();
176
+ onClose();
177
+ }
178
+ };
179
+ document.addEventListener("keydown", handleKeyDown);
180
+ return () => document.removeEventListener("keydown", handleKeyDown);
181
+ }, [open, onClose]);
182
+ const sizeConfig = PANEL_SIZE_CONFIG[panelSize];
723
183
  const transport = useMemo(() => new DefaultChatTransport({
724
184
  api: config.apiEndpoint ?? "/api/chat",
725
185
  prepareSendMessagesRequest: ({ id, messages: msgs }) => ({
@@ -738,11 +198,95 @@ function ChatPanel({ open, onClose, config, articleContext }) {
738
198
  setMessages: liveSetMessages,
739
199
  regenerate,
740
200
  status: liveStatus,
741
- error: liveError
201
+ error: liveError,
202
+ addToolOutput
742
203
  } = useChat({
743
204
  transport,
744
- onError: (err) => {
745
- console.error("[ChatPanel] Chat error:", err.message);
205
+ sendAutomaticallyWhen: shouldAutoContinueAfterToolCalls,
206
+ async onToolCall({ toolCall }) {
207
+ const executor = window.__actionExecutor;
208
+ if (!executor) {
209
+ console.warn("[ChatPanel] ActionExecutor not initialized");
210
+ addToolOutput({
211
+ tool: toolCall.toolName,
212
+ toolCallId: toolCall.toolCallId,
213
+ output: {
214
+ success: false,
215
+ tool: toolCall.toolName,
216
+ error: "ActionExecutor not initialized"
217
+ }
218
+ });
219
+ return;
220
+ }
221
+ const TOOL_ACTION_MAP = {
222
+ toggleTheme: (i) => ({ type: "toggle-theme", payload: { theme: i.theme } }),
223
+ navigateToArticle: (i) => ({
224
+ type: "navigate",
225
+ payload: {
226
+ slug: i.slug,
227
+ lang: i.lang || "zh",
228
+ then: i.sectionId ? [{ type: "scroll-to-section", payload: { sectionId: i.sectionId } }] : void 0
229
+ }
230
+ }),
231
+ scrollToSection: (i) => ({
232
+ type: "scroll-to-section",
233
+ payload: { sectionId: i.sectionId, highlight: i.highlight ?? true, behavior: i.behavior ?? "smooth" }
234
+ }),
235
+ toggleReadingMode: (i) => ({
236
+ type: "toggle-reading-mode",
237
+ payload: {
238
+ enabled: i.enabled,
239
+ settings: { ...i.fontSize ? { fontSize: i.fontSize } : {}, ...i.fontFamily ? { fontFamily: i.fontFamily } : {} }
240
+ }
241
+ }),
242
+ highlightText: (i) => ({
243
+ type: "highlight-text",
244
+ payload: { text: i.text, selector: i.selector, style: i.style ?? "accent", duration: i.duration ?? 3e3, scrollIntoView: i.scrollIntoView ?? false }
245
+ }),
246
+ setPreference: (i) => ({ type: "set-preference", payload: { key: i.key, value: i.value } })
247
+ };
248
+ const mapper = TOOL_ACTION_MAP[toolCall.toolName];
249
+ if (!mapper) {
250
+ console.warn("[ChatPanel] Unknown tool:", toolCall.toolName);
251
+ addToolOutput({
252
+ tool: toolCall.toolName,
253
+ toolCallId: toolCall.toolCallId,
254
+ output: {
255
+ success: false,
256
+ tool: toolCall.toolName,
257
+ error: `Unknown tool: ${toolCall.toolName}`
258
+ }
259
+ });
260
+ return;
261
+ }
262
+ try {
263
+ const toolInput = toolCall.input ?? {};
264
+ const action = mapper(toolInput);
265
+ const result = await executor.execute(action);
266
+ addToolOutput({
267
+ tool: toolCall.toolName,
268
+ toolCallId: toolCall.toolCallId,
269
+ output: {
270
+ success: result.success,
271
+ tool: toolCall.toolName,
272
+ action: action.type,
273
+ input: toolInput,
274
+ result,
275
+ confirmation: result.success ? `Tool ${toolCall.toolName} executed successfully.` : `Tool ${toolCall.toolName} failed: ${result.error ?? "unknown error"}`
276
+ }
277
+ });
278
+ } catch (error2) {
279
+ console.error("[ChatPanel] Tool execution error:", error2);
280
+ addToolOutput({
281
+ tool: toolCall.toolName,
282
+ toolCallId: toolCall.toolCallId,
283
+ output: {
284
+ success: false,
285
+ tool: toolCall.toolName,
286
+ error: String(error2)
287
+ }
288
+ });
289
+ }
746
290
  }
747
291
  });
748
292
  useEffect(() => {
@@ -753,29 +297,9 @@ function ChatPanel({ open, onClose, config, articleContext }) {
753
297
  const mockChat = useMockChat(lang);
754
298
  const isStreaming = isMockMode ? mockChat.isStreaming : liveStatus === "streaming" || liveStatus === "submitted";
755
299
  const error = isMockMode ? null : liveError;
756
- useEffect(() => {
757
- if (isMockMode || !liveMessages.length) return;
758
- for (let i = liveMessages.length - 1; i >= 0; i--) {
759
- const msg = liveMessages[i];
760
- if (msg.role === "assistant" && isChatStatusData(msg.metadata)) {
761
- setStatusMessage(msg.metadata.message);
762
- return;
763
- }
764
- }
765
- setStatusMessage(void 0);
766
- }, [liveMessages, isMockMode]);
767
300
  useEffect(() => {
768
301
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
769
302
  }, [liveMessages, mockChat.messages]);
770
- useEffect(() => {
771
- if (open) setTimeout(() => inputRef.current?.focus(), 150);
772
- }, [open]);
773
- const autoResize = useCallback(() => {
774
- const el = inputRef.current;
775
- if (!el) return;
776
- el.style.height = "auto";
777
- el.style.height = Math.min(el.scrollHeight, 96) + "px";
778
- }, []);
779
303
  const doSend = useCallback((text) => {
780
304
  const trimmed = text.trim();
781
305
  if (!trimmed || isStreaming || cooldown) return;
@@ -785,7 +309,7 @@ function ChatPanel({ open, onClose, config, articleContext }) {
785
309
  setCooldown(true);
786
310
  setTimeout(() => setCooldown(false), MIN_SEND_INTERVAL_MS);
787
311
  setInputValue("");
788
- if (inputRef.current) inputRef.current.style.height = "auto";
312
+ resetInputHeight();
789
313
  if (isMockMode) {
790
314
  mockChat.sendMessage(trimmed);
791
315
  } else {
@@ -793,12 +317,6 @@ function ChatPanel({ open, onClose, config, articleContext }) {
793
317
  }
794
318
  }, [isStreaming, cooldown, isMockMode, mockChat, liveSendMessage]);
795
319
  const handleSend = useCallback(() => doSend(inputValue), [doSend, inputValue]);
796
- const handleKeyDown = useCallback((e) => {
797
- if (e.key === "Enter" && !e.shiftKey) {
798
- e.preventDefault();
799
- handleSend();
800
- }
801
- }, [handleSend]);
802
320
  const handleClear = useCallback(() => {
803
321
  if (isMockMode) {
804
322
  mockChat.clear();
@@ -806,8 +324,12 @@ function ChatPanel({ open, onClose, config, articleContext }) {
806
324
  liveSetMessages([welcomeMessage]);
807
325
  }
808
326
  setInputValue("");
809
- setStatusMessage(void 0);
810
327
  }, [isMockMode, mockChat, liveSetMessages, welcomeMessage]);
328
+ const handleClose = useCallback(() => {
329
+ onClose();
330
+ window.setTimeout(() => lastChatTrigger?.focus(), 0);
331
+ }, [onClose]);
332
+ const statusMessage = error ? parseErrorMessage(error, lang) : isStreaming ? t("ai.status.generating", lang) : "";
811
333
  if (!open) return null;
812
334
  const renderMockMessages = () => /* @__PURE__ */ h(Fragment, null, mockChat.messages.length === 0 && /* @__PURE__ */ h("div", { class: "space-y-3" }, /* @__PURE__ */ h("div", { class: "flex items-start gap-2.5" }, /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("p", { class: "min-w-0 flex-1 pt-0.5 text-[13px] leading-relaxed text-foreground" }, getTextFromMessage(welcomeMessage))), /* @__PURE__ */ h("div", { class: "flex flex-wrap gap-1.5 pl-8" }, quickPrompts.map((q) => /* @__PURE__ */ h(
813
335
  "button",
@@ -818,7 +340,7 @@ function ChatPanel({ open, onClose, config, articleContext }) {
818
340
  class: "rounded-lg border border-border bg-muted/30 px-2.5 py-1 text-[12px] text-foreground-soft transition-colors hover:border-accent/40 hover:bg-accent/10 hover:text-foreground"
819
341
  },
820
342
  q
821
- )))), mockChat.messages.map((msg) => /* @__PURE__ */ h("div", { key: msg.id, class: msg.role === "user" ? "flex justify-end" : "flex items-start gap-2.5" }, msg.role === "assistant" && /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: msg.role === "user" ? "max-w-[82%] rounded-2xl rounded-br-md bg-accent px-3 py-2 text-[13px] leading-relaxed text-background" : "min-w-0 flex-1 pt-0.5 text-[13px] leading-relaxed text-foreground" }, msg.text ? msg.role === "assistant" ? /* @__PURE__ */ h(RichText, { text: msg.text }) : msg.text : msg.streaming ? /* @__PURE__ */ h(TypingDots, null) : null))));
343
+ )))), mockChat.messages.map((msg) => /* @__PURE__ */ h("div", { key: msg.id, class: msg.role === "user" ? "flex justify-end" : "flex items-start gap-2.5" }, msg.role === "assistant" && /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: msg.role === "user" ? "max-w-[82%] rounded-2xl rounded-br-md bg-accent px-3 py-2 text-[13px] leading-relaxed text-background" : "min-w-0 flex-1 pt-0.5 text-[13px] leading-relaxed text-foreground" }, msg.text ? msg.role === "assistant" ? /* @__PURE__ */ h(RichText, { text: msg.text, isStreaming: msg.streaming }) : msg.text : msg.streaming ? /* @__PURE__ */ h(TypingDots, null) : null))));
822
344
  const renderLiveMessages = () => {
823
345
  const showQuickPrompts = liveMessages.length <= 1;
824
346
  const lastAssistantMsgId = [...liveMessages].reverse().find((m) => m.role === "assistant")?.id;
@@ -840,7 +362,16 @@ function ChatPanel({ open, onClose, config, articleContext }) {
840
362
  const text = getTextFromMessage(msg);
841
363
  const isAssistant = msg.role === "assistant";
842
364
  const isLastAssistantStreaming = isStreaming && msg.id === lastAssistantMsgId;
843
- return /* @__PURE__ */ h("div", { key: msg.id, class: msg.role === "user" ? "flex justify-end" : "flex items-start gap-2.5" }, isAssistant && /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: msg.role === "user" ? "max-w-[82%] rounded-2xl rounded-br-md bg-accent px-3 py-2 text-[13px] leading-relaxed text-background" : "min-w-0 flex-1 pt-0.5 text-[13px] leading-relaxed text-foreground" }, isAssistant ? /* @__PURE__ */ h(AssistantMessage, { message: msg, isStreaming: isLastAssistantStreaming, lang }) : text));
365
+ return /* @__PURE__ */ h("div", { key: msg.id, class: msg.role === "user" ? "flex justify-end" : "flex items-start gap-2.5" }, isAssistant && /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: msg.role === "user" ? "max-w-[82%] rounded-2xl rounded-br-md bg-accent px-3 py-2 text-[13px] leading-relaxed text-background" : "min-w-0 flex-1 pt-0.5 text-[13px] leading-relaxed text-foreground" }, isAssistant ? /* @__PURE__ */ h(
366
+ AssistantMessage,
367
+ {
368
+ message: msg,
369
+ isStreaming: isLastAssistantStreaming,
370
+ lang,
371
+ articleContext,
372
+ onFollowUp: doSend
373
+ }
374
+ ) : text));
844
375
  }), isWaitingForAssistant && /* @__PURE__ */ h("div", { class: "flex items-start gap-2.5" }, /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: "min-w-0 flex-1 pt-0.5" }, /* @__PURE__ */ h(ReasoningBlock, { text: "", isStreaming: true, lang }))));
845
376
  };
846
377
  return /* @__PURE__ */ h(
@@ -849,29 +380,48 @@ function ChatPanel({ open, onClose, config, articleContext }) {
849
380
  ref: panelRef,
850
381
  id: "ai-chat-panel",
851
382
  "data-ai-chat-panel": true,
852
- class: "fixed right-4 bottom-20 z-[90] flex w-[370px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-2xl sm:right-6 sm:bottom-20",
853
- style: { height: "min(520px, calc(100vh - 7rem))" }
383
+ role: "dialog",
384
+ "aria-modal": "false",
385
+ "aria-labelledby": titleId,
386
+ "aria-describedby": descriptionId,
387
+ class: `fixed z-[90] flex flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-2xl transition-all duration-300 ease-out ${panelSize === "L" ? "right-[10vw] bottom-[10vh] z-[100]" : "right-4 bottom-20 sm:right-6 sm:bottom-20"} ${sizeConfig.class}`,
388
+ style: { height: sizeConfig.height }
854
389
  },
855
- /* @__PURE__ */ h("div", { class: "flex shrink-0 items-center justify-between border-b border-border px-3.5 py-2.5" }, /* @__PURE__ */ h("div", { class: "flex items-center gap-2" }, /* @__PURE__ */ h("div", { class: "flex size-6 shrink-0 items-center justify-center rounded-full bg-accent/15" }, /* @__PURE__ */ h(BotIcon, { class: "size-3 text-accent" })), /* @__PURE__ */ h("div", { class: "flex flex-col" }, /* @__PURE__ */ h("span", { class: "text-[13px] font-semibold text-foreground" }, t("ai.assistantName", lang)), articleContext && /* @__PURE__ */ h("span", { class: "max-w-[180px] truncate text-[10px] text-foreground-soft" }, t("ai.header.reading", lang), articleContext.title)), /* @__PURE__ */ h("span", { class: `rounded-full px-1.5 py-px text-[10px] font-medium ${isMockMode ? "bg-amber-500/15 text-amber-600 dark:text-amber-400" : "bg-green-500/15 text-green-600 dark:text-green-400"}` }, isMockMode ? t("ai.header.mode", lang) : t("ai.status.live", lang))), /* @__PURE__ */ h("div", { class: "flex items-center gap-0.5" }, /* @__PURE__ */ h(
390
+ /* @__PURE__ */ h("div", { class: "flex shrink-0 items-center justify-between border-b border-border px-3.5 py-2.5" }, /* @__PURE__ */ h("div", { class: "flex items-center gap-2" }, /* @__PURE__ */ h("div", { class: "flex size-6 shrink-0 items-center justify-center rounded-full bg-accent/15" }, /* @__PURE__ */ h(BotIcon, { class: "size-3 text-accent", "aria-hidden": "true" })), /* @__PURE__ */ h("div", { class: "flex flex-col" }, /* @__PURE__ */ h("span", { id: titleId, class: "text-[13px] font-semibold text-foreground" }, t("ai.assistantName", lang)), articleContext && /* @__PURE__ */ h("span", { id: descriptionId, class: "max-w-[180px] truncate text-[10px] text-foreground-soft" }, t("ai.header.reading", lang), articleContext.title), !articleContext && /* @__PURE__ */ h("span", { id: descriptionId, class: "sr-only" }, placeholder)), /* @__PURE__ */ h("span", { class: `rounded-full px-1.5 py-px text-[10px] font-medium ${isMockMode ? "bg-amber-500/15 text-amber-600 dark:text-amber-400" : "bg-green-500/15 text-green-600 dark:text-green-400"}` }, isMockMode ? t("ai.header.mode", lang) : t("ai.status.live", lang))), /* @__PURE__ */ h("div", { class: "flex items-center gap-0.5" }, /* @__PURE__ */ h("div", { class: "flex items-center gap-0.5 rounded-md border border-border bg-muted/30 p-0.5" }, ["S", "M", "L"].map((size) => /* @__PURE__ */ h(
391
+ "button",
392
+ {
393
+ key: size,
394
+ type: "button",
395
+ onClick: () => setPanelSize(size),
396
+ class: `rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${panelSize === size ? "bg-accent text-background" : "text-foreground-soft hover:text-foreground"}`,
397
+ title: size === "S" ? "Small" : size === "M" ? "Medium" : "Large",
398
+ "aria-label": size === "S" ? "Small chat panel" : size === "M" ? "Medium chat panel" : "Large chat panel",
399
+ "aria-pressed": panelSize === size
400
+ },
401
+ size
402
+ ))), /* @__PURE__ */ h(
856
403
  "button",
857
404
  {
858
405
  type: "button",
859
406
  onClick: handleClear,
407
+ "aria-label": t("ai.clear", lang),
860
408
  class: "rounded-md p-1 text-foreground-soft transition-colors hover:bg-muted/60 hover:text-foreground",
861
409
  title: t("ai.clear", lang)
862
410
  },
863
- /* @__PURE__ */ h("svg", { class: "size-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("path", { d: "M3 6h18" }), /* @__PURE__ */ h("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), /* @__PURE__ */ h("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }))
411
+ /* @__PURE__ */ h("svg", { class: "size-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, /* @__PURE__ */ h("path", { d: "M3 6h18" }), /* @__PURE__ */ h("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), /* @__PURE__ */ h("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }))
864
412
  ), /* @__PURE__ */ h(
865
413
  "button",
866
414
  {
867
415
  type: "button",
868
- onClick: onClose,
416
+ onClick: handleClose,
417
+ "aria-label": t("ai.close", lang),
869
418
  class: "rounded-md p-1 text-foreground-soft transition-colors hover:bg-muted/60 hover:text-foreground",
870
419
  title: t("ai.close", lang)
871
420
  },
872
- /* @__PURE__ */ h("svg", { class: "size-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("path", { d: "M18 6 6 18" }), /* @__PURE__ */ h("path", { d: "m6 6 12 12" }))
421
+ /* @__PURE__ */ h("svg", { class: "size-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, /* @__PURE__ */ h("path", { d: "M18 6 6 18" }), /* @__PURE__ */ h("path", { d: "m6 6 12 12" }))
873
422
  ))),
874
- /* @__PURE__ */ h("div", { class: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-3.5 py-3 [scrollbar-width:thin]" }, /* @__PURE__ */ h("div", { class: "space-y-4" }, isMockMode ? renderMockMessages() : renderLiveMessages(), error && /* @__PURE__ */ h("div", { class: "flex items-start gap-2.5" }, /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: "flex flex-col gap-1 pt-0.5" }, /* @__PURE__ */ h("p", { class: "text-[13px] text-amber-600 dark:text-amber-400" }, parseErrorMessage(error, lang)), isRetryable(error) && /* @__PURE__ */ h(
423
+ /* @__PURE__ */ h("div", { id: statusId, class: "sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true" }, statusMessage),
424
+ /* @__PURE__ */ h("div", { class: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-3.5 py-3 [scrollbar-width:thin]", "aria-live": "off" }, /* @__PURE__ */ h("div", { class: "space-y-4" }, isMockMode ? renderMockMessages() : renderLiveMessages(), error && /* @__PURE__ */ h("div", { class: "flex items-start gap-2.5", role: "alert" }, /* @__PURE__ */ h(BotAvatar, null), /* @__PURE__ */ h("div", { class: "flex flex-col gap-1 pt-0.5" }, /* @__PURE__ */ h("p", { class: "text-[13px] text-amber-600 dark:text-amber-400" }, parseErrorMessage(error, lang)), isRetryable(error) && /* @__PURE__ */ h(
875
425
  "button",
876
426
  {
877
427
  type: "button",
@@ -880,33 +430,21 @@ function ChatPanel({ open, onClose, config, articleContext }) {
880
430
  },
881
431
  t("ai.retry", lang)
882
432
  ))), /* @__PURE__ */ h("div", { ref: messagesEndRef }))),
883
- /* @__PURE__ */ h("div", { class: "shrink-0 border-t border-border px-3 pb-2.5 pt-2" }, /* @__PURE__ */ h("div", { class: "flex items-end gap-1.5 rounded-xl border border-border bg-muted/30 px-2.5 py-1.5 transition-colors focus-within:border-accent/40 focus-within:bg-background" }, /* @__PURE__ */ h(
884
- "textarea",
433
+ /* @__PURE__ */ h(
434
+ ChatInput,
885
435
  {
886
- id: "ai-chat-input",
887
- ref: inputRef,
888
- rows: 1,
889
436
  value: inputValue,
890
- onInput: (e) => {
891
- setInputValue(e.target.value);
892
- autoResize();
893
- },
894
- onKeyDown: handleKeyDown,
437
+ onInput: setInputValue,
438
+ onSend: handleSend,
439
+ isStreaming,
440
+ cooldown,
895
441
  placeholder,
896
- maxLength: 500,
897
- class: "min-w-0 flex-1 resize-none bg-transparent py-0.5 text-[13px] leading-snug text-foreground outline-none placeholder:text-foreground-soft",
898
- style: { maxHeight: "96px" }
442
+ label: t("ai.placeholder", lang),
443
+ sendLabel: getChatActionLabel(lang, "send"),
444
+ sendingLabel: getChatActionLabel(lang, "sending"),
445
+ focusTrigger: open
899
446
  }
900
- ), /* @__PURE__ */ h(
901
- "button",
902
- {
903
- type: "button",
904
- onClick: handleSend,
905
- disabled: !inputValue.trim() || isStreaming || cooldown,
906
- class: "mb-0.5 flex size-6 shrink-0 items-center justify-center rounded-md bg-accent text-background transition-all hover:bg-accent/90 disabled:cursor-not-allowed disabled:opacity-30"
907
- },
908
- isStreaming ? /* @__PURE__ */ h("svg", { class: "size-3 animate-spin", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2.5" }, /* @__PURE__ */ h("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })) : /* @__PURE__ */ h("svg", { class: "size-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2.5", "stroke-linecap": "round", "stroke-linejoin": "round" }, /* @__PURE__ */ h("path", { d: "m22 2-7 20-4-9-9-4Z" }), /* @__PURE__ */ h("path", { d: "M22 2 11 13" }))
909
- )))
447
+ )
910
448
  );
911
449
  }
912
450
  export {