@astro-minimax/ai 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astro-minimax/ai",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "description": "Vendor-agnostic AI integration package with full RAG pipeline for astro-minimax blogs — supports OpenAI, Cloudflare AI, and custom providers.",
6
6
  "author": "Souloss",
@@ -76,6 +76,9 @@
76
76
  "files": [
77
77
  "dist/",
78
78
  "src/components/",
79
+ "src/providers/mock.ts",
80
+ "src/server/types.ts",
81
+ "src/utils/i18n.ts",
79
82
  "src/styles/",
80
83
  "README.md"
81
84
  ],
@@ -84,7 +87,7 @@
84
87
  "@ai-sdk/openai-compatible": "^2.0.35",
85
88
  "ai": "^6.0.116",
86
89
  "workers-ai-provider": "^3.1.2",
87
- "@astro-minimax/notify": "0.7.2"
90
+ "@astro-minimax/notify": "0.7.4"
88
91
  },
89
92
  "optionalDependencies": {
90
93
  "undici": "^6.0.0"
@@ -112,7 +115,7 @@
112
115
  "pnpm": ">=9.0.0"
113
116
  },
114
117
  "scripts": {
115
- "build": "tsc -p tsconfig.build.json",
118
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/server/dev-server.js",
116
119
  "build:watch": "tsc -p tsconfig.build.json --watch",
117
120
  "typecheck": "tsc --noEmit",
118
121
  "clean": "rm -rf dist"
@@ -1,7 +1,8 @@
1
+ /** @jsxImportSource preact */
1
2
  import { useState, useCallback } from 'preact/hooks';
2
- import { ChatPanel } from './ChatPanel.js';
3
- import type { AIChatConfig } from './ChatPanel.js';
4
- import type { ArticleChatContext } from '../server/types.js';
3
+ import { ChatPanel } from './ChatPanel.tsx';
4
+ import type { AIChatConfig } from './ChatPanel.tsx';
5
+ import type { ArticleChatContext } from '../server/types.ts';
5
6
 
6
7
  interface Props {
7
8
  config: AIChatConfig;
@@ -1,11 +1,12 @@
1
+ /** @jsxImportSource preact */
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
2
3
  import { useChat } from '@ai-sdk/react';
3
4
  import { DefaultChatTransport } from 'ai';
4
5
  import type { UIMessage } from 'ai';
5
- import { getMockResponse, createMockStream } from '../providers/mock.js';
6
- import type { ArticleChatContext, ChatStatusData } from '../server/types.js';
7
- import { isChatStatusData } from '../server/types.js';
8
- import { t, getLang } from '../utils/i18n.js';
6
+ import { getMockResponse, createMockStream } from '../providers/mock.ts';
7
+ import type { ArticleChatContext, ChatStatusData } from '../server/types.ts';
8
+ import { isChatStatusData } from '../server/types.ts';
9
+ import { t, getLang } from '../utils/i18n.ts';
9
10
 
10
11
  export interface AIChatConfig {
11
12
  enabled?: boolean;
@@ -775,7 +776,7 @@ export function ChatPanel({ open, onClose, config, articleContext }: ChatPanelPr
775
776
  };
776
777
 
777
778
  return (
778
- <div ref={panelRef} data-ai-chat-panel
779
+ <div ref={panelRef} id="ai-chat-panel" data-ai-chat-panel
779
780
  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"
780
781
  style={{ height: 'min(520px, calc(100vh - 7rem))' }}>
781
782
 
@@ -844,7 +845,7 @@ export function ChatPanel({ open, onClose, config, articleContext }: ChatPanelPr
844
845
  {/* Input Area */}
845
846
  <div class="shrink-0 border-t border-border px-3 pb-2.5 pt-2">
846
847
  <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">
847
- <textarea ref={inputRef} rows={1} value={inputValue}
848
+ <textarea id="ai-chat-input" ref={inputRef} rows={1} value={inputValue}
848
849
  onInput={(e) => { setInputValue((e.target as HTMLTextAreaElement).value); autoResize(); }}
849
850
  onKeyDown={handleKeyDown} placeholder={placeholder} maxLength={500}
850
851
  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"
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Mock provider for development and testing.
3
+ * Returns predefined responses with article/link recommendations.
4
+ * Responses use Markdown links which the ChatPanel renders as clickable elements.
5
+ */
6
+
7
+ const MOCK_RESPONSES: Array<{ patterns: RegExp[]; zh: string; en: string }> = [
8
+ {
9
+ patterns: [/astro/i, /框架/],
10
+ zh: `Astro 是一个现代化的静态站点生成器,核心优势是"岛屿架构"——默认零 JS,只在交互组件上加载脚本。本博客基于 Astro 构建。
11
+
12
+ 推荐阅读:
13
+ - [快速上手:两种集成方式](/zh/posts/getting-started) — 了解如何搭建 astro-minimax 博客
14
+ - [如何配置主题](/zh/posts/how-to-configure-astro-minimax-theme) — 自定义你的博客外观
15
+
16
+ 外部资源:
17
+ - [Astro 官方文档](https://docs.astro.build) — 深入学习 Astro 框架
18
+ - [Astro 主题市场](https://astro.build/themes/) — 发现更多 Astro 主题`,
19
+ en: `Astro is a modern static site generator with an "Islands Architecture" — zero JS by default, loading scripts only for interactive components. This blog is built with Astro.
20
+
21
+ Recommended reading:
22
+ - [Getting Started: Two Integration Methods](/en/posts/getting-started) — Learn how to set up an astro-minimax blog
23
+ - [How to Configure the Theme](/en/posts/how-to-configure-astro-minimax-theme) — Customize your blog
24
+
25
+ External resources:
26
+ - [Astro Documentation](https://docs.astro.build) — Learn Astro in depth
27
+ - [Astro Themes](https://astro.build/themes/) — Discover more themes`,
28
+ },
29
+ {
30
+ patterns: [/推荐|文章|看什么|读什么|recommend/i],
31
+ zh: `以下是一些热门文章推荐:
32
+
33
+ **入门系列:**
34
+ - [快速上手:两种集成方式](/zh/posts/getting-started) — 搭建你的第一个博客
35
+ - [如何添加新文章](/zh/posts/adding-new-post) — 内容创作指南
36
+ - [预定义配色方案](/zh/posts/predefined-color-schemes) — 选一个你喜欢的主题色
37
+
38
+ **技术深度:**
39
+ - [如何在博客中使用 LaTeX 公式](/zh/posts/how-to-add-latex-equations-in-blog-posts) — 数学公式支持
40
+ - [动态 OG 图片生成](/zh/posts/dynamic-og-images) — 自动生成社交分享图
41
+
42
+ 你对哪个方向的内容更感兴趣?我可以做更精准的推荐。`,
43
+ en: `Here are some recommended articles:
44
+
45
+ **Getting Started:**
46
+ - [Getting Started: Two Integration Methods](/en/posts/getting-started) — Build your first blog
47
+ - [Adding New Posts](/en/posts/adding-new-post) — Content creation guide
48
+ - [Predefined Color Schemes](/en/posts/predefined-color-schemes) — Pick your favorite theme color
49
+
50
+ **Technical Deep Dives:**
51
+ - [LaTeX Equations in Blog Posts](/en/posts/how-to-add-latex-equations-in-blog-posts) — Math formula support
52
+ - [Dynamic OG Images](/en/posts/dynamic-og-images) — Auto-generate social share images
53
+
54
+ What direction interests you more? I can provide more specific recommendations.`,
55
+ },
56
+ {
57
+ patterns: [/博客|blog|功能|feature/i],
58
+ zh: `这个博客基于 **astro-minimax** 主题,功能丰富:
59
+
60
+ 核心功能:Markdown/MDX、代码高亮、[数学公式(KaTeX)](/zh/posts/how-to-add-latex-equations-in-blog-posts)、[Mermaid 图表](/zh/posts/mermaid-diagrams)、标签分类、全文搜索(Pagefind)、[Waline 评论](https://waline.js.org)、深色模式。
61
+
62
+ 了解更多:
63
+ - [配置指南](/zh/posts/how-to-configure-astro-minimax-theme) — 完整配置选项
64
+ - [Markdown 扩展语法](/zh/posts/markdown-extended) — 所有支持的语法特性
65
+
66
+ 开源地址:[souloss/astro-minimax](https://github.com/souloss/astro-minimax)`,
67
+ en: `This blog uses the **astro-minimax** theme with rich features:
68
+
69
+ 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.
70
+
71
+ Learn more:
72
+ - [Configuration Guide](/en/posts/how-to-configure-astro-minimax-theme) — Full config options
73
+ - [Extended Markdown](/en/posts/markdown-extended) — All supported syntax features
74
+
75
+ Open source: [souloss/astro-minimax](https://github.com/souloss/astro-minimax)`,
76
+ },
77
+ {
78
+ patterns: [/主题|theme|暗色|dark|颜色|color|配色/i],
79
+ zh: `博客支持亮色和暗色主题,右下角按钮即可切换,也会自动检测系统偏好。
80
+
81
+ 配色方案可以在配置中自定义,目前提供多种预设:
82
+ - [预定义配色方案](/zh/posts/predefined-color-schemes) — 查看所有可用配色
83
+ - [自定义主题色](/zh/posts/customizing-astro-minimax-theme-color-schemes) — 创建你自己的配色
84
+
85
+ 参考 [Tailwind CSS 调色板](https://tailwindcss.com/docs/customizing-colors) 获取灵感。`,
86
+ en: `The blog supports light and dark themes — toggle with the bottom-right button or auto-detect system preference.
87
+
88
+ Color schemes are customizable:
89
+ - [Predefined Color Schemes](/en/posts/predefined-color-schemes) — See all available schemes
90
+ - [Custom Theme Colors](/en/posts/customizing-astro-minimax-theme-color-schemes) — Create your own
91
+
92
+ Check [Tailwind CSS Color Palette](https://tailwindcss.com/docs/customizing-colors) for inspiration.`,
93
+ },
94
+ {
95
+ patterns: [/搭建|部署|deploy|build|install|安装|搭/i],
96
+ zh: `搭建类似的博客非常简单!有两种方式:
97
+
98
+ 1. **GitHub 模板**(推荐新手)— 一键 Fork,开箱即用
99
+ 2. **NPM 包集成** — 适合内容与系统分离的进阶用法
100
+
101
+ 详细步骤请看 [快速上手](/zh/posts/getting-started)。
102
+
103
+ 部署推荐 [Cloudflare Pages](https://pages.cloudflare.com)(免费、全球 CDN),也支持 [Vercel](https://vercel.com) 和 [Netlify](https://netlify.com)。`,
104
+ en: `Setting up a similar blog is easy! Two methods:
105
+
106
+ 1. **GitHub Template** (recommended for beginners) — One-click fork, ready to use
107
+ 2. **NPM Package Integration** — For advanced content/system separation
108
+
109
+ See [Getting Started](/en/posts/getting-started) for detailed steps.
110
+
111
+ Deploy with [Cloudflare Pages](https://pages.cloudflare.com) (free, global CDN), or [Vercel](https://vercel.com) / [Netlify](https://netlify.com).`,
112
+ },
113
+ {
114
+ patterns: [/rust/i],
115
+ zh: `博客中有一系列 Rust 文章:
116
+ - [Rust 入门介绍](/zh/posts/rust-series-01-introduction) — 语言基础
117
+ - [所有权系统](/zh/posts/rust-series-02-ownership) — Rust 核心概念
118
+ - [错误处理](/zh/posts/rust-series-03-error-handling) — Result 和 Option
119
+ - [并发编程](/zh/posts/rust-series-04-concurrency) — 安全的多线程
120
+
121
+ 外部学习资源:
122
+ - [The Rust Book](https://doc.rust-lang.org/book/) — 官方教程
123
+ - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) — 实例学习`,
124
+ en: `The blog has a Rust series:
125
+ - [Rust Introduction](/en/posts/rust-series-01-introduction) — Language basics
126
+ - [Ownership System](/en/posts/rust-series-02-ownership) — Core Rust concept
127
+ - [Error Handling](/en/posts/rust-series-03-error-handling) — Result and Option
128
+ - [Concurrency](/en/posts/rust-series-04-concurrency) — Safe multithreading
129
+
130
+ External resources:
131
+ - [The Rust Book](https://doc.rust-lang.org/book/) — Official tutorial
132
+ - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) — Learn by examples`,
133
+ },
134
+ {
135
+ patterns: [/ai|人工智能|助手|assistant|chat/i],
136
+ zh: `我是这个博客的 AI 助手!当前运行在 Demo 模式,可以:
137
+ - 根据你的问题推荐相关博客文章
138
+ - 推荐有用的外部学习资源
139
+ - 解答关于博客技术栈的问题
140
+
141
+ 启用完整 AI 功能(RAG 搜索增强)需要配置 \`AI_BASE_URL\` 和 \`AI_API_KEY\` 环境变量。
142
+
143
+ 试试问我:"有哪些文章推荐?" 或 "怎么搭建类似的博客?"`,
144
+ en: `I'm the blog AI assistant! Currently in Demo mode, I can:
145
+ - Recommend relevant blog articles based on your questions
146
+ - Suggest useful external learning resources
147
+ - Answer questions about the blog's tech stack
148
+
149
+ For full AI features (RAG search enhancement), configure \`AI_BASE_URL\` and \`AI_API_KEY\` environment variables.
150
+
151
+ Try asking: "Recommend some articles?" or "How to build a similar blog?"`,
152
+ },
153
+ {
154
+ patterns: [/搜索|search|pagefind/i],
155
+ zh: `博客集成了 [Pagefind](https://pagefind.app) 全文搜索引擎,构建时自动索引。点击页面顶部搜索图标即可使用。
156
+
157
+ 了解更多搜索功能:
158
+ - [Pagefind 官方文档](https://pagefind.app/docs/) — 完整配置指南
159
+ - 搜索支持中文和英文内容`,
160
+ 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.
161
+
162
+ Learn more:
163
+ - [Pagefind Documentation](https://pagefind.app/docs/) — Complete configuration guide
164
+ - Search supports both Chinese and English content`,
165
+ },
166
+ {
167
+ patterns: [/markdown|mdx|语法|syntax|公式|latex|mermaid|图表/i],
168
+ zh: `博客支持丰富的内容语法:
169
+
170
+ - [Markdown 基础语法](/zh/posts/markdown-basics) — 标题、列表、表格等
171
+ - [Markdown 扩展语法](/zh/posts/markdown-extended) — 脚注、高亮、折叠等
172
+ - [LaTeX 数学公式](/zh/posts/how-to-add-latex-equations-in-blog-posts) — KaTeX 渲染
173
+ - [Mermaid 图表](/zh/posts/mermaid-diagrams) — 流程图、时序图
174
+ - [Markmap 思维导图](/zh/posts/markmap-mindmaps) — 交互式思维导图
175
+
176
+ 外部参考:[GitHub Flavored Markdown](https://github.github.com/gfm/)`,
177
+ en: `The blog supports rich content syntax:
178
+
179
+ - [Markdown Basics](/en/posts/markdown-basics) — Headings, lists, tables
180
+ - [Extended Markdown](/en/posts/markdown-extended) — Footnotes, highlights, collapsible
181
+ - [LaTeX Equations](/en/posts/how-to-add-latex-equations-in-blog-posts) — KaTeX rendering
182
+ - [Mermaid Diagrams](/en/posts/mermaid-diagrams) — Flowcharts, sequence diagrams
183
+ - [Markmap Mind Maps](/en/posts/markmap-mindmaps) — Interactive mind maps
184
+
185
+ Reference: [GitHub Flavored Markdown](https://github.github.com/gfm/)`,
186
+ },
187
+ ];
188
+
189
+ const FALLBACK = {
190
+ zh: `感谢提问!我目前在 Demo 模式下,可以推荐博客文章和外部资源。
191
+
192
+ 试试这些话题:
193
+ - "有哪些文章推荐?"
194
+ - "Astro 框架是什么?"
195
+ - "怎么搭建类似的博客?"
196
+ - "支持哪些 Markdown 语法?"`,
197
+ en: `Thanks for asking! I'm in Demo mode and can recommend blog articles and external resources.
198
+
199
+ Try these topics:
200
+ - "Recommend some articles?"
201
+ - "What is Astro?"
202
+ - "How to build a similar blog?"
203
+ - "What Markdown syntax is supported?"`,
204
+ };
205
+
206
+ /**
207
+ * Returns a mock response with Markdown links for article/external link recommendations.
208
+ */
209
+ export function getMockResponse(question: string, lang = 'zh'): string {
210
+ const q = question.toLowerCase();
211
+ const isZh = lang !== 'en';
212
+
213
+ for (const { patterns, zh, en } of MOCK_RESPONSES) {
214
+ if (patterns.some(p => p.test(q))) {
215
+ return isZh ? zh : en;
216
+ }
217
+ }
218
+
219
+ return isZh ? FALLBACK.zh : FALLBACK.en;
220
+ }
221
+
222
+ /**
223
+ * Creates a ReadableStream that simulates character-by-character streaming.
224
+ */
225
+ export function createMockStream(text: string): ReadableStream<string> {
226
+ let index = 0;
227
+ return new ReadableStream<string>({
228
+ async pull(controller) {
229
+ if (index >= text.length) {
230
+ controller.close();
231
+ return;
232
+ }
233
+ const chunkSize = Math.random() < 0.3 ? 2 : 1;
234
+ const chunk = text.slice(index, index + chunkSize);
235
+ index += chunkSize;
236
+ controller.enqueue(chunk);
237
+ await new Promise(resolve => setTimeout(resolve, 12 + Math.random() * 23));
238
+ },
239
+ });
240
+ }
@@ -0,0 +1,89 @@
1
+ import type { UIMessage } from 'ai';
2
+ import type { ProviderManagerEnv } from '../provider-manager/types.js';
3
+ import type { CacheEnv } from '../cache/types.js';
4
+
5
+ // ── Chat Context ──────────────────────────────────────────────
6
+
7
+ export interface ChatContext {
8
+ scope: 'global' | 'article';
9
+ article?: ArticleChatContext;
10
+ }
11
+
12
+ export interface ArticleChatContext {
13
+ slug: string;
14
+ title: string;
15
+ categories?: string[];
16
+ summary?: string;
17
+ abstract?: string;
18
+ keyPoints?: string[];
19
+ relatedSlugs?: string[];
20
+ }
21
+
22
+ // ── Request / Response ────────────────────────────────────────
23
+
24
+ export interface ChatRequestBody {
25
+ context?: ChatContext;
26
+ id?: string;
27
+ messages: UIMessage[];
28
+ lang?: string;
29
+ }
30
+
31
+ export interface ChatHandlerEnv extends ProviderManagerEnv, CacheEnv {
32
+ SITE_AUTHOR?: string;
33
+ SITE_URL?: string;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ export interface ChatHandlerOptions {
38
+ env: ChatHandlerEnv;
39
+ request: Request;
40
+ waitUntil?: (promise: Promise<unknown>) => void;
41
+ }
42
+
43
+ // ── Status Metadata ───────────────────────────────────────────
44
+
45
+ export type ChatStatusStage = 'search' | 'answer' | 'complete';
46
+
47
+ export interface ChatStatusData {
48
+ stage: ChatStatusStage;
49
+ message: string;
50
+ progress: number;
51
+ done: boolean;
52
+ at: number;
53
+ }
54
+
55
+ export function createChatStatusData(
56
+ partial: Omit<ChatStatusData, 'done' | 'at'> & { done?: boolean },
57
+ ): ChatStatusData {
58
+ return {
59
+ ...partial,
60
+ done: partial.done ?? partial.stage === 'complete',
61
+ at: Date.now(),
62
+ };
63
+ }
64
+
65
+ export function isChatStatusData(value: unknown): value is ChatStatusData {
66
+ if (!value || typeof value !== 'object') return false;
67
+ const v = value as Record<string, unknown>;
68
+ return typeof v.stage === 'string' && typeof v.message === 'string' && typeof v.progress === 'number';
69
+ }
70
+
71
+ // ── Error Response ────────────────────────────────────────────
72
+
73
+ export interface ChatErrorResponse {
74
+ error: string;
75
+ code: string;
76
+ retryable: boolean;
77
+ retryAfter?: number;
78
+ }
79
+
80
+ // ── Metadata Initialization ───────────────────────────────────
81
+
82
+ export interface MetadataConfig {
83
+ summaries: unknown;
84
+ authorContext: unknown;
85
+ voiceProfile: unknown;
86
+ factRegistry?: unknown;
87
+ vectorIndex?: unknown;
88
+ siteUrl?: string;
89
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * AI Package Internationalization
3
+ * Follows the same pattern as packages/core/src/utils/i18n.ts
4
+ */
5
+
6
+ export type AITranslationKey =
7
+ // Reasoning UI
8
+ | "ai.reasoning.thinking"
9
+ | "ai.reasoning.viewReasoning"
10
+ | "ai.reasoning.waiting"
11
+ // Error messages
12
+ | "ai.error.network"
13
+ | "ai.error.aborted"
14
+ | "ai.error.rateLimit"
15
+ | "ai.error.unavailable"
16
+ | "ai.error.generic"
17
+ | "ai.error.format"
18
+ | "ai.error.noOutput"
19
+ // UI labels
20
+ | "ai.placeholder"
21
+ | "ai.clear"
22
+ | "ai.clearConversation"
23
+ | "ai.close"
24
+ | "ai.closeChat"
25
+ | "ai.retry"
26
+ | "ai.status.searching"
27
+ | "ai.status.generating"
28
+ | "ai.status.found"
29
+ | "ai.status.citation"
30
+ | "ai.status.fallback"
31
+ // Quick prompts
32
+ | "ai.prompt.techStack"
33
+ | "ai.prompt.recommend"
34
+ | "ai.prompt.build"
35
+ | "ai.prompt.summarize"
36
+ | "ai.prompt.explain"
37
+ | "ai.prompt.related"
38
+ // Welcome messages
39
+ | "ai.welcome.reading"
40
+ | "ai.welcome.canHelp"
41
+ | "ai.welcome.greeting"
42
+ | "ai.welcome.demo"
43
+ | "ai.welcome.demoHint"
44
+ | "ai.welcome.demoPrompt"
45
+ // Header
46
+ | "ai.header.reading"
47
+ | "ai.header.mode"
48
+ // Assistant branding
49
+ | "ai.assistantName"
50
+ | "ai.status.live"
51
+ // Additional error messages
52
+ | "ai.error.emptyMessage"
53
+ | "ai.error.emptyContent"
54
+ | "ai.error.inputTooLong"
55
+ | "ai.error.timeout"
56
+ // Rate limit messages
57
+ | "ai.error.rateLimit.burst"
58
+ | "ai.error.rateLimit.sustained"
59
+ | "ai.error.rateLimit.daily"
60
+ // Prompt section titles
61
+ | "ai.prompt.section.responsibilities"
62
+ | "ai.prompt.section.format"
63
+ | "ai.prompt.section.principles"
64
+ | "ai.prompt.section.constraints"
65
+ | "ai.prompt.section.sourceLayers"
66
+ | "ai.prompt.section.privacy"
67
+ | "ai.prompt.section.answerModes"
68
+ | "ai.prompt.section.preOutputChecks"
69
+ // Semi-static layer labels
70
+ | "ai.semiStatic.blogOverview"
71
+ | "ai.semiStatic.totalPosts"
72
+ | "ai.semiStatic.mainCategories"
73
+ | "ai.semiStatic.latestArticles";
74
+
75
+ const translations: Record<string, Record<AITranslationKey, string>> = {
76
+ en: {
77
+ // Reasoning UI
78
+ "ai.reasoning.thinking": "Thinking...",
79
+ "ai.reasoning.viewReasoning": "View reasoning",
80
+ "ai.reasoning.waiting": "Waiting for thoughts...",
81
+ // Error messages
82
+ "ai.error.network": "Network connection failed. Please check your connection.",
83
+ "ai.error.aborted": "Request was cancelled.",
84
+ "ai.error.rateLimit": "Too many requests. Please try again later.",
85
+ "ai.error.unavailable": "AI service is temporarily unavailable.",
86
+ "ai.error.generic": "Something went wrong. Please try again later.",
87
+ "ai.error.format": "Invalid request format.",
88
+ // UI labels
89
+ "ai.placeholder": "Ask a question...",
90
+ "ai.clear": "Clear",
91
+ "ai.clearConversation": "Clear conversation",
92
+ "ai.close": "Close",
93
+ "ai.closeChat": "Close chat",
94
+ "ai.retry": "Retry",
95
+ "ai.status.searching": "Searching...",
96
+ "ai.status.generating": "Generating response...",
97
+ "ai.status.found": "Found {count} related items",
98
+ "ai.status.citation": "Answered from public records",
99
+ "ai.status.fallback": "AI service unavailable, using demo mode",
100
+ // Quick prompts
101
+ "ai.prompt.techStack": "What tech stack is used?",
102
+ "ai.prompt.recommend": "Recommend some articles?",
103
+ "ai.prompt.build": "How to build a similar blog?",
104
+ "ai.prompt.summarize": 'Summarize the key points of "{title}"',
105
+ "ai.prompt.explain": 'Explain "{point}"',
106
+ "ai.prompt.related": "What related content should I read next?",
107
+ // Welcome messages
108
+ "ai.welcome.reading": 'I\'m reading "{title}" with you.\nAsk me to summarize, explain a concept, or explore related topics.',
109
+ "ai.welcome.canHelp": "Hi! I'm the blog AI assistant. Ask me anything and I'll help you find related articles.",
110
+ "ai.welcome.greeting": "Hi! I'm the blog AI assistant.",
111
+ "ai.welcome.demo": "I'm running in demo mode. I can recommend blog articles and external resources.",
112
+ "ai.welcome.demoHint": "For full AI features (RAG search), configure AI_BASE_URL and AI_API_KEY.",
113
+ "ai.welcome.demoPrompt": 'Try: "Recommend articles?" or "How to build this blog?"',
114
+ // Header
115
+ "ai.header.reading": "Reading:",
116
+ "ai.header.mode": "Demo",
117
+ // Assistant branding
118
+ "ai.assistantName": "Blog Avatar",
119
+ "ai.status.live": "Live",
120
+ // Additional error messages
121
+ "ai.error.emptyMessage": "Message cannot be empty.",
122
+ "ai.error.emptyContent": "Message content cannot be empty.",
123
+ "ai.error.inputTooLong": "Message too long, max {max} characters.",
124
+ "ai.error.timeout": "Response timeout, please retry or simplify your question.",
125
+ // Rate limit messages
126
+ "ai.error.rateLimit.burst": "Too many requests, please try again later.",
127
+ "ai.error.rateLimit.sustained": "Too many requests, please wait a minute.",
128
+ "ai.error.rateLimit.daily": "Daily limit reached, please come back tomorrow.",
129
+ "ai.error.noOutput": "Sorry, I could not generate a valid response. Please try rephrasing your question.",
130
+ "ai.prompt.section.responsibilities": "Your Responsibilities",
131
+ "ai.prompt.section.format": "Response Format",
132
+ "ai.prompt.section.principles": "Recommendation Principles",
133
+ "ai.prompt.section.constraints": "Constraints",
134
+ "ai.prompt.section.sourceLayers": "Source Priority Protocol (must follow)",
135
+ "ai.prompt.section.privacy": "Privacy Protection",
136
+ "ai.prompt.section.answerModes": "Answer Mode Guide (follow detected mode)",
137
+ "ai.prompt.section.preOutputChecks": "Pre-Output Checks (execute mentally, do not output steps)",
138
+ "ai.semiStatic.blogOverview": "Blog Overview",
139
+ "ai.semiStatic.totalPosts": "{count} posts total",
140
+ "ai.semiStatic.mainCategories": "Main categories: {categories}",
141
+ "ai.semiStatic.latestArticles": "Latest Posts",
142
+ },
143
+ zh: {
144
+ // Reasoning UI
145
+ "ai.reasoning.thinking": "思考中...",
146
+ "ai.reasoning.viewReasoning": "查看思考过程",
147
+ "ai.reasoning.waiting": "等待思考...",
148
+ // Error messages
149
+ "ai.error.network": "网络连接失败,请检查网络",
150
+ "ai.error.aborted": "请求已取消",
151
+ "ai.error.rateLimit": "请求太频繁,请稍后再试",
152
+ "ai.error.unavailable": "AI 服务暂时不可用",
153
+ "ai.error.generic": "出了点问题,请稍后再试",
154
+ "ai.error.format": "请求格式错误",
155
+ // UI labels
156
+ "ai.placeholder": "输入你的问题...",
157
+ "ai.clear": "清除",
158
+ "ai.clearConversation": "清除对话",
159
+ "ai.close": "关闭",
160
+ "ai.closeChat": "关闭聊天",
161
+ "ai.retry": "重试",
162
+ "ai.status.searching": "搜索中...",
163
+ "ai.status.generating": "正在生成回答...",
164
+ "ai.status.found": "找到 {count} 篇相关内容",
165
+ "ai.status.citation": "已基于公开记录直接给出回答",
166
+ "ai.status.fallback": "AI 服务不可用,使用演示模式回复",
167
+ // Quick prompts
168
+ "ai.prompt.techStack": "这个博客用了什么技术?",
169
+ "ai.prompt.recommend": "有哪些文章推荐?",
170
+ "ai.prompt.build": "怎么搭建类似的博客?",
171
+ "ai.prompt.summarize": "总结一下《{title}》的核心观点",
172
+ "ai.prompt.explain": "解释一下「{point}」",
173
+ "ai.prompt.related": "这篇文章和哪些内容相关?",
174
+ // Welcome messages
175
+ "ai.welcome.reading": "我在结合《{title}》陪你阅读。\n你可以让我总结这篇文章、解释某个观点,或者顺着这篇文章继续延伸到相关主题。",
176
+ "ai.welcome.canHelp": "你好!我是博客 AI 助手,问我任何关于博客内容的问题,我可以帮你找到相关文章。",
177
+ "ai.welcome.greeting": "你好!我是博客 AI 助手。",
178
+ "ai.welcome.demo": "我目前在 Demo 模式下,可以推荐博客文章和外部资源。",
179
+ "ai.welcome.demoHint": "启用完整 AI 功能(RAG 搜索增强)需要配置 AI_BASE_URL 和 AI_API_KEY 环境变量。",
180
+ "ai.welcome.demoPrompt": "试试:「有哪些文章推荐?」或「怎么搭建类似的博客?」",
181
+ // Header
182
+ "ai.header.reading": "正在阅读:",
183
+ "ai.header.mode": "演示",
184
+ // Assistant branding
185
+ "ai.assistantName": "博客分身",
186
+ "ai.status.live": "在线",
187
+ // Additional error messages
188
+ "ai.error.emptyMessage": "消息不能为空。",
189
+ "ai.error.emptyContent": "消息内容不能为空。",
190
+ "ai.error.inputTooLong": "消息过长,最多 {max} 字。",
191
+ "ai.error.timeout": "响应超时,请重试或简化问题。",
192
+ // Rate limit messages
193
+ "ai.error.rateLimit.burst": "请求太频繁,请稍后再试。",
194
+ "ai.error.rateLimit.sustained": "请求次数过多,请一分钟后再试。",
195
+ "ai.error.rateLimit.daily": "今日对话次数已达上限,请明天再来。",
196
+ "ai.error.noOutput": "抱歉,我无法生成有效的回答。请尝试换一种方式提问。",
197
+ "ai.prompt.section.responsibilities": "你的职责",
198
+ "ai.prompt.section.format": "回答格式",
199
+ "ai.prompt.section.principles": "推荐原则",
200
+ "ai.prompt.section.constraints": "约束",
201
+ "ai.prompt.section.sourceLayers": "来源分层协议(必须遵守)",
202
+ "ai.prompt.section.privacy": "隐私保护",
203
+ "ai.prompt.section.answerModes": "回答模式指导(按检测到的模式执行)",
204
+ "ai.prompt.section.preOutputChecks": "输出前检查(在心里执行,不输出步骤)",
205
+ "ai.semiStatic.blogOverview": "博客概况",
206
+ "ai.semiStatic.totalPosts": "共有 {count} 篇文章",
207
+ "ai.semiStatic.mainCategories": "主要分类:{categories}",
208
+ "ai.semiStatic.latestArticles": "最新文章",
209
+ },
210
+ };
211
+
212
+ /**
213
+ * Get translation by key.
214
+ * @param key - Translation key (type-safe)
215
+ * @param lang - Language code ('zh' or 'en')
216
+ * @param vars - Optional variables for interpolation (e.g., { count: 5 })
217
+ */
218
+ export function t(key: AITranslationKey, lang: string = 'zh', vars?: Record<string, string | number>): string {
219
+ const l = lang === 'zh' ? 'zh' : 'en';
220
+ let text = translations[l]?.[key] ?? translations['en'][key] ?? key;
221
+
222
+ // Interpolate variables like {count}, {title}, etc.
223
+ if (vars) {
224
+ for (const [k, v] of Object.entries(vars)) {
225
+ text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
226
+ }
227
+ }
228
+
229
+ return text;
230
+ }
231
+
232
+ /**
233
+ * Get normalized language code.
234
+ * Returns 'zh' for Chinese, 'en' for everything else.
235
+ */
236
+ export function getLang(lang?: string): string {
237
+ return lang === 'zh' ? 'zh' : 'en';
238
+ }