@astro-minimax/ai 0.7.4 → 0.8.0

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 (80) hide show
  1. package/dist/components/AIChatContainer.d.ts +9 -0
  2. package/dist/components/AIChatContainer.d.ts.map +1 -0
  3. package/dist/components/AIChatContainer.js +936 -0
  4. package/{src → dist}/components/AIChatWidget.astro +1 -1
  5. package/dist/components/ChatPanel.d.ts +19 -0
  6. package/dist/components/ChatPanel.d.ts.map +1 -0
  7. package/dist/components/ChatPanel.js +914 -0
  8. package/dist/data/index.js +18 -1
  9. package/dist/fact-registry/index.js +16 -3
  10. package/dist/index.js +11 -30
  11. package/dist/intelligence/evidence-analysis.d.ts.map +1 -1
  12. package/dist/intelligence/index.js +56 -5
  13. package/dist/intelligence/keyword-extract.d.ts.map +1 -1
  14. package/dist/middleware/index.js +10 -1
  15. package/dist/prompt/index.js +10 -4
  16. package/dist/provider-manager/base.d.ts +1 -0
  17. package/dist/provider-manager/base.d.ts.map +1 -1
  18. package/dist/provider-manager/types.d.ts +1 -0
  19. package/dist/provider-manager/types.d.ts.map +1 -1
  20. package/dist/providers/index.js +5 -1
  21. package/dist/search/index.js +48 -6
  22. package/dist/server/dev-server.js +236 -259
  23. package/dist/server/index.js +39 -6
  24. package/dist/stream/index.js +8 -2
  25. package/package.json +16 -10
  26. package/dist/cache/global-cache.js +0 -141
  27. package/dist/cache/index.js +0 -62
  28. package/dist/cache/kv-adapter.js +0 -102
  29. package/dist/cache/memory-adapter.js +0 -95
  30. package/dist/cache/response-cache.js +0 -85
  31. package/dist/cache/types.js +0 -16
  32. package/dist/data/metadata-loader.js +0 -66
  33. package/dist/data/types.js +0 -1
  34. package/dist/fact-registry/fact-matcher.js +0 -94
  35. package/dist/fact-registry/prompt-injector.js +0 -57
  36. package/dist/fact-registry/registry.js +0 -38
  37. package/dist/fact-registry/types.js +0 -5
  38. package/dist/intelligence/citation-appender.js +0 -65
  39. package/dist/intelligence/citation-guard.js +0 -125
  40. package/dist/intelligence/evidence-analysis.js +0 -88
  41. package/dist/intelligence/intent-detect.js +0 -131
  42. package/dist/intelligence/keyword-extract.js +0 -114
  43. package/dist/intelligence/response-templates.js +0 -116
  44. package/dist/intelligence/types.js +0 -1
  45. package/dist/middleware/rate-limiter.js +0 -129
  46. package/dist/prompt/dynamic-layer.js +0 -67
  47. package/dist/prompt/prompt-builder.js +0 -12
  48. package/dist/prompt/semi-static-layer.js +0 -29
  49. package/dist/prompt/static-layer.js +0 -150
  50. package/dist/prompt/types.js +0 -1
  51. package/dist/provider-manager/base.js +0 -47
  52. package/dist/provider-manager/config.js +0 -134
  53. package/dist/provider-manager/index.js +0 -6
  54. package/dist/provider-manager/manager.js +0 -121
  55. package/dist/provider-manager/mock.js +0 -56
  56. package/dist/provider-manager/openai.js +0 -112
  57. package/dist/provider-manager/types.js +0 -6
  58. package/dist/provider-manager/workers.js +0 -74
  59. package/dist/providers/mock.js +0 -234
  60. package/dist/search/idf.js +0 -31
  61. package/dist/search/search-api.js +0 -119
  62. package/dist/search/search-index.js +0 -35
  63. package/dist/search/search-utils.js +0 -122
  64. package/dist/search/session-cache.js +0 -92
  65. package/dist/search/types.js +0 -1
  66. package/dist/search/vector-reranker.js +0 -135
  67. package/dist/server/chat-handler.js +0 -590
  68. package/dist/server/errors.js +0 -41
  69. package/dist/server/metadata-init.js +0 -47
  70. package/dist/server/notify.js +0 -74
  71. package/dist/server/stream-helpers.js +0 -197
  72. package/dist/server/types.js +0 -13
  73. package/dist/stream/mock-stream.js +0 -27
  74. package/dist/stream/response.js +0 -22
  75. package/dist/utils/i18n.js +0 -164
  76. package/src/components/AIChatContainer.tsx +0 -31
  77. package/src/components/ChatPanel.tsx +0 -866
  78. package/src/providers/mock.ts +0 -240
  79. package/src/server/types.ts +0 -89
  80. package/src/utils/i18n.ts +0 -238
@@ -0,0 +1,914 @@
1
+ // src/components/ChatPanel.tsx
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
3
+ import { h, Fragment } from 'preact';
4
+
5
+ import { useChat } from "@ai-sdk/react";
6
+ 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
+ - [\u81EA\u5B9A\u4E49\u4E3B\u9898\u8272](/zh/posts/customizing-astro-minimax-theme-color-schemes) \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
+ - [Custom Theme Colors](/en/posts/customizing-astro-minimax-theme-color-schemes) \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
+ }
213
+ }
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);
443
+ }
444
+ function generateSessionId(articleContext) {
445
+ if (articleContext?.slug) return `article:${articleContext.slug}`;
446
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
447
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
448
+ }
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 = [
454
+ t("ai.prompt.techStack", "zh"),
455
+ t("ai.prompt.recommend", "zh"),
456
+ t("ai.prompt.build", "zh")
457
+ ];
458
+ var QUICK_PROMPTS_EN = [
459
+ t("ai.prompt.techStack", "en"),
460
+ t("ai.prompt.recommend", "en"),
461
+ t("ai.prompt.build", "en")
462
+ ];
463
+ function getQuickPrompts(lang, articleContext) {
464
+ const l = getLang(lang);
465
+ if (!articleContext) return l === "zh" ? QUICK_PROMPTS_ZH : QUICK_PROMPTS_EN;
466
+ if (l === "zh") {
467
+ const prompts2 = [t("ai.prompt.summarize", "zh", { title: articleContext.title })];
468
+ if (articleContext.keyPoints?.length) {
469
+ prompts2.push(t("ai.prompt.explain", "zh", { point: articleContext.keyPoints[0] }));
470
+ }
471
+ prompts2.push(t("ai.prompt.related", "zh"));
472
+ return prompts2;
473
+ }
474
+ const prompts = [t("ai.prompt.summarize", "en", { title: articleContext.title })];
475
+ if (articleContext.keyPoints?.length) {
476
+ prompts.push(t("ai.prompt.explain", "en", { point: articleContext.keyPoints[0] }));
477
+ }
478
+ prompts.push(t("ai.prompt.related", "en"));
479
+ return prompts;
480
+ }
481
+ function buildWelcomeMessage(config, articleContext) {
482
+ const lang = getLang(config.lang);
483
+ let text;
484
+ if (articleContext) {
485
+ text = config.welcomeMessage ?? t("ai.welcome.reading", lang, { title: articleContext.title });
486
+ } else {
487
+ text = config.welcomeMessage ?? t("ai.welcome.canHelp", lang);
488
+ }
489
+ return {
490
+ id: "welcome",
491
+ role: "assistant",
492
+ parts: [{ type: "text", text }]
493
+ };
494
+ }
495
+ function parseErrorMessage(error, lang = "zh") {
496
+ const l = getLang(lang);
497
+ try {
498
+ const parsed = JSON.parse(error.message);
499
+ if (parsed?.error) return parsed.error;
500
+ } catch {
501
+ }
502
+ const msg = error.message;
503
+ if (msg.includes("Failed to fetch") || msg.includes("NetworkError")) return t("ai.error.network", l);
504
+ if (msg.includes("aborted")) return t("ai.error.aborted", l);
505
+ if (msg.includes("429") || msg.includes("rate")) return t("ai.error.rateLimit", l);
506
+ if (msg.includes("503") || msg.includes("unavailable")) return t("ai.error.unavailable", l);
507
+ return t("ai.error.generic", l);
508
+ }
509
+ function isRetryable(error) {
510
+ try {
511
+ const parsed = JSON.parse(error.message);
512
+ if (typeof parsed?.retryable === "boolean") return parsed.retryable;
513
+ } catch {
514
+ }
515
+ return true;
516
+ }
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
+ function useMockChat(lang) {
683
+ const [messages, setMessages] = useState([]);
684
+ const [isStreaming, setIsStreaming] = useState(false);
685
+ const sendMessage = useCallback(async (text) => {
686
+ const userMsg = { id: `u-${Date.now()}`, role: "user", text };
687
+ const assistantId = `a-${Date.now()}`;
688
+ const assistantMsg = { id: assistantId, role: "assistant", text: "", streaming: true };
689
+ setMessages((prev) => [...prev, userMsg, assistantMsg]);
690
+ setIsStreaming(true);
691
+ const stream = createMockStream(getMockResponse(text, lang));
692
+ const reader = stream.getReader();
693
+ let accumulated = "";
694
+ try {
695
+ while (true) {
696
+ const { done, value } = await reader.read();
697
+ if (done) break;
698
+ accumulated += value;
699
+ setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, text: accumulated } : m));
700
+ }
701
+ } finally {
702
+ setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, streaming: false } : m));
703
+ setIsStreaming(false);
704
+ }
705
+ }, [lang]);
706
+ const clear = useCallback(() => setMessages([]), []);
707
+ return { messages, isStreaming, sendMessage, clear };
708
+ }
709
+ function ChatPanel({ open, onClose, config, articleContext }) {
710
+ const isMockMode = config.mockMode || !config.apiEndpoint;
711
+ const lang = getLang(config.lang);
712
+ const placeholder = config.placeholder ?? t("ai.placeholder", lang);
713
+ const sessionId = useMemo(() => generateSessionId(articleContext), [articleContext]);
714
+ const panelRef = useRef(null);
715
+ const inputRef = useRef(null);
716
+ const messagesEndRef = useRef(null);
717
+ const lastSendRef = useRef(0);
718
+ const [inputValue, setInputValue] = useState("");
719
+ const [cooldown, setCooldown] = useState(false);
720
+ const [statusMessage, setStatusMessage] = useState();
721
+ const quickPrompts = useMemo(() => getQuickPrompts(lang, articleContext), [lang, articleContext]);
722
+ const welcomeMessage = useMemo(() => buildWelcomeMessage(config, articleContext), [config, articleContext]);
723
+ const transport = useMemo(() => new DefaultChatTransport({
724
+ api: config.apiEndpoint ?? "/api/chat",
725
+ prepareSendMessagesRequest: ({ id, messages: msgs }) => ({
726
+ headers: { "x-session-id": sessionId },
727
+ body: {
728
+ id,
729
+ messages: msgs,
730
+ lang,
731
+ context: articleContext ? { scope: "article", article: articleContext } : { scope: "global" }
732
+ }
733
+ })
734
+ }), [config.apiEndpoint, sessionId, articleContext, lang]);
735
+ const {
736
+ messages: liveMessages,
737
+ sendMessage: liveSendMessage,
738
+ setMessages: liveSetMessages,
739
+ regenerate,
740
+ status: liveStatus,
741
+ error: liveError
742
+ } = useChat({
743
+ transport,
744
+ onError: (err) => {
745
+ console.error("[ChatPanel] Chat error:", err.message);
746
+ }
747
+ });
748
+ useEffect(() => {
749
+ if (liveMessages.length === 0) {
750
+ liveSetMessages([welcomeMessage]);
751
+ }
752
+ }, []);
753
+ const mockChat = useMockChat(lang);
754
+ const isStreaming = isMockMode ? mockChat.isStreaming : liveStatus === "streaming" || liveStatus === "submitted";
755
+ 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
+ useEffect(() => {
768
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
769
+ }, [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
+ const doSend = useCallback((text) => {
780
+ const trimmed = text.trim();
781
+ if (!trimmed || isStreaming || cooldown) return;
782
+ const now = Date.now();
783
+ if (now - lastSendRef.current < MIN_SEND_INTERVAL_MS) return;
784
+ lastSendRef.current = now;
785
+ setCooldown(true);
786
+ setTimeout(() => setCooldown(false), MIN_SEND_INTERVAL_MS);
787
+ setInputValue("");
788
+ if (inputRef.current) inputRef.current.style.height = "auto";
789
+ if (isMockMode) {
790
+ mockChat.sendMessage(trimmed);
791
+ } else {
792
+ liveSendMessage({ text: trimmed });
793
+ }
794
+ }, [isStreaming, cooldown, isMockMode, mockChat, liveSendMessage]);
795
+ 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
+ const handleClear = useCallback(() => {
803
+ if (isMockMode) {
804
+ mockChat.clear();
805
+ } else {
806
+ liveSetMessages([welcomeMessage]);
807
+ }
808
+ setInputValue("");
809
+ setStatusMessage(void 0);
810
+ }, [isMockMode, mockChat, liveSetMessages, welcomeMessage]);
811
+ if (!open) return null;
812
+ 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
+ "button",
814
+ {
815
+ key: q,
816
+ type: "button",
817
+ onClick: () => doSend(q),
818
+ 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
+ },
820
+ 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))));
822
+ const renderLiveMessages = () => {
823
+ const showQuickPrompts = liveMessages.length <= 1;
824
+ const lastAssistantMsgId = [...liveMessages].reverse().find((m) => m.role === "assistant")?.id;
825
+ const lastMessage = liveMessages[liveMessages.length - 1];
826
+ const isWaitingForAssistant = isStreaming && lastMessage?.role === "user";
827
+ return /* @__PURE__ */ h(Fragment, null, liveMessages.map((msg) => {
828
+ if (msg.id === "welcome" && showQuickPrompts) {
829
+ return /* @__PURE__ */ h("div", { key: msg.id, 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(msg))), /* @__PURE__ */ h("div", { class: "flex flex-wrap gap-1.5 pl-8" }, quickPrompts.map((q) => /* @__PURE__ */ h(
830
+ "button",
831
+ {
832
+ key: q,
833
+ type: "button",
834
+ onClick: () => doSend(q),
835
+ 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"
836
+ },
837
+ q
838
+ ))));
839
+ }
840
+ const text = getTextFromMessage(msg);
841
+ const isAssistant = msg.role === "assistant";
842
+ 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));
844
+ }), 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
+ };
846
+ return /* @__PURE__ */ h(
847
+ "div",
848
+ {
849
+ ref: panelRef,
850
+ id: "ai-chat-panel",
851
+ "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))" }
854
+ },
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(
856
+ "button",
857
+ {
858
+ type: "button",
859
+ onClick: handleClear,
860
+ class: "rounded-md p-1 text-foreground-soft transition-colors hover:bg-muted/60 hover:text-foreground",
861
+ title: t("ai.clear", lang)
862
+ },
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" }))
864
+ ), /* @__PURE__ */ h(
865
+ "button",
866
+ {
867
+ type: "button",
868
+ onClick: onClose,
869
+ class: "rounded-md p-1 text-foreground-soft transition-colors hover:bg-muted/60 hover:text-foreground",
870
+ title: t("ai.close", lang)
871
+ },
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" }))
873
+ ))),
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(
875
+ "button",
876
+ {
877
+ type: "button",
878
+ onClick: () => regenerate(),
879
+ class: "self-start rounded-md border border-amber-500/30 px-2 py-0.5 text-[11px] text-amber-600 transition-colors hover:bg-amber-500/10 dark:text-amber-400"
880
+ },
881
+ t("ai.retry", lang)
882
+ ))), /* @__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",
885
+ {
886
+ id: "ai-chat-input",
887
+ ref: inputRef,
888
+ rows: 1,
889
+ value: inputValue,
890
+ onInput: (e) => {
891
+ setInputValue(e.target.value);
892
+ autoResize();
893
+ },
894
+ onKeyDown: handleKeyDown,
895
+ 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" }
899
+ }
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
+ )))
910
+ );
911
+ }
912
+ export {
913
+ ChatPanel
914
+ };