@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
@@ -1,74 +0,0 @@
1
- import { createNotifier } from '@astro-minimax/notify';
2
- let notifierInstance = null;
3
- function getNotifier(env) {
4
- if (notifierInstance)
5
- return notifierInstance;
6
- const hasConfig = env.NOTIFY_TELEGRAM_BOT_TOKEN || env.NOTIFY_WEBHOOK_URL || env.NOTIFY_RESEND_API_KEY;
7
- if (!hasConfig) {
8
- console.warn('[notify] No notification providers configured. Missing environment variables: NOTIFY_TELEGRAM_BOT_TOKEN, NOTIFY_WEBHOOK_URL, or NOTIFY_RESEND_API_KEY');
9
- return null;
10
- }
11
- const providers = [];
12
- if (env.NOTIFY_TELEGRAM_BOT_TOKEN && env.NOTIFY_TELEGRAM_CHAT_ID)
13
- providers.push('telegram');
14
- if (env.NOTIFY_WEBHOOK_URL)
15
- providers.push('webhook');
16
- if (env.NOTIFY_RESEND_API_KEY && env.NOTIFY_RESEND_FROM && env.NOTIFY_RESEND_TO)
17
- providers.push('email');
18
- console.log(`[notify] Initializing notifier with providers: ${providers.join(', ') || 'none'}`);
19
- notifierInstance = createNotifier({
20
- telegram: env.NOTIFY_TELEGRAM_BOT_TOKEN && env.NOTIFY_TELEGRAM_CHAT_ID ? {
21
- botToken: env.NOTIFY_TELEGRAM_BOT_TOKEN,
22
- chatId: env.NOTIFY_TELEGRAM_CHAT_ID,
23
- } : undefined,
24
- webhook: env.NOTIFY_WEBHOOK_URL ? {
25
- url: env.NOTIFY_WEBHOOK_URL,
26
- } : undefined,
27
- email: env.NOTIFY_RESEND_API_KEY && env.NOTIFY_RESEND_FROM && env.NOTIFY_RESEND_TO ? {
28
- provider: 'resend',
29
- apiKey: env.NOTIFY_RESEND_API_KEY,
30
- from: env.NOTIFY_RESEND_FROM,
31
- to: env.NOTIFY_RESEND_TO,
32
- } : undefined,
33
- });
34
- return notifierInstance;
35
- }
36
- function getMessageText(message) {
37
- if (Array.isArray(message.parts)) {
38
- return message.parts
39
- .filter((p) => p.type === 'text')
40
- .map(p => p.text)
41
- .join('');
42
- }
43
- return '';
44
- }
45
- export function notifyAiChat(options) {
46
- const { env, sessionId, messages, aiResponse, referencedArticles, model, usage, timing } = options;
47
- const notifier = getNotifier(env);
48
- if (!notifier) {
49
- console.warn('[notify] AI chat notification skipped: no notifier available. Check environment variables.');
50
- return Promise.resolve(null);
51
- }
52
- const userMessages = messages.filter(m => m.role === 'user');
53
- const lastUserMessage = userMessages[userMessages.length - 1];
54
- if (!lastUserMessage) {
55
- console.warn('[notify] AI chat notification skipped: no user message found in messages array');
56
- return Promise.resolve(null);
57
- }
58
- const userMessage = getMessageText(lastUserMessage);
59
- const roundNumber = userMessages.length;
60
- return notifier.aiChat({
61
- sessionId,
62
- roundNumber,
63
- userMessage,
64
- aiResponse: aiResponse?.slice(0, 500),
65
- referencedArticles,
66
- model,
67
- usage,
68
- timing,
69
- siteUrl: env.SITE_URL,
70
- }).catch((error) => {
71
- console.error('[notify] AI chat notification failed:', error);
72
- return null;
73
- });
74
- }
@@ -1,197 +0,0 @@
1
- /**
2
- * Stream helper utilities for chat-handler.
3
- *
4
- * Extracts duplicated stream-writing logic into reusable functions,
5
- * eliminating 34+ `as never` casts and reducing chat-handler.ts size.
6
- */
7
- import { streamText, convertToModelMessages, } from 'ai';
8
- import { t } from '../utils/i18n.js';
9
- import { createChatStatusData } from './types.js';
10
- import { createResponsePlaybackGenerator } from '../cache/response-cache.js';
11
- // ── Metadata Writers ──────────────────────────────────────
12
- export function writeSearchStatus(writer, count, lang) {
13
- writer.write({
14
- type: 'message-metadata',
15
- messageMetadata: createChatStatusData({
16
- stage: 'search',
17
- message: t('ai.status.found', lang, { count }),
18
- progress: 40,
19
- }),
20
- });
21
- }
22
- export function writeGeneratingStatus(writer, lang, progress = 60) {
23
- writer.write({
24
- type: 'message-metadata',
25
- messageMetadata: createChatStatusData({
26
- stage: 'answer',
27
- message: t('ai.status.generating', lang),
28
- progress,
29
- }),
30
- });
31
- }
32
- export function writeDoneStatus(writer, lang) {
33
- writer.write({
34
- type: 'message-metadata',
35
- messageMetadata: createChatStatusData({
36
- stage: 'answer',
37
- message: t('ai.status.generating', lang),
38
- progress: 100,
39
- done: true,
40
- }),
41
- });
42
- }
43
- export function writeSourceArticles(writer, articles, max = 3) {
44
- for (const article of articles.slice(0, max)) {
45
- try {
46
- writer.write({
47
- type: 'source-url',
48
- sourceId: `source-${article.title}`,
49
- url: article.url ?? '#',
50
- title: article.title,
51
- });
52
- }
53
- catch { /* best-effort */ }
54
- }
55
- }
56
- export function writeTextChunk(writer, text, idPrefix = 'text') {
57
- const id = `${idPrefix}-${Date.now()}`;
58
- writer.write({ type: 'text-start', id });
59
- writer.write({ type: 'text-delta', id, delta: text });
60
- writer.write({ type: 'text-end', id });
61
- }
62
- export function writeFinish(writer, reason = 'stop') {
63
- writer.write({ type: 'finish', finishReason: reason });
64
- }
65
- // ── LLM Streaming ─────────────────────────────────────────
66
- export async function streamLLMResponse(params) {
67
- const { writer, adapter, systemPrompt, messages, lang, temperature = 0.3, maxOutputTokens = 2500, } = params;
68
- const start = Date.now();
69
- try {
70
- const provider = adapter.getProvider();
71
- const result = streamText({
72
- model: provider.chatModel(adapter.model),
73
- system: systemPrompt,
74
- messages: await convertToModelMessages(messages),
75
- temperature,
76
- maxOutputTokens,
77
- onError: ({ error }) => {
78
- console.error('[stream-helpers] streamText error:', error);
79
- },
80
- });
81
- const streamErrors = [];
82
- writer.merge(result.toUIMessageStream({ sendFinish: false }));
83
- await result.consumeStream({
84
- onError: (error) => {
85
- streamErrors.push(error instanceof Error ? error : new Error(String(error)));
86
- },
87
- });
88
- const text = await result.text;
89
- let reasoningText;
90
- const reasoningPromise = result.reasoning;
91
- if (reasoningPromise) {
92
- try {
93
- const reasoningOutput = await Promise.resolve(reasoningPromise);
94
- reasoningText = typeof reasoningOutput === 'string'
95
- ? reasoningOutput
96
- : (Array.isArray(reasoningOutput)
97
- ? reasoningOutput.map((r) => {
98
- if (typeof r === 'object' && r !== null && 'text' in r)
99
- return r.text;
100
- return String(r);
101
- }).join('')
102
- : undefined);
103
- }
104
- catch { /* reasoning is optional */ }
105
- }
106
- let tokenUsage;
107
- const usagePromise = result.usage;
108
- if (usagePromise) {
109
- try {
110
- const usage = await Promise.resolve(usagePromise);
111
- const inputTokens = usage.inputTokens ?? 0;
112
- const outputTokens = usage.outputTokens ?? 0;
113
- tokenUsage = {
114
- total: usage.totalTokens ?? inputTokens + outputTokens,
115
- input: inputTokens,
116
- output: outputTokens,
117
- };
118
- }
119
- catch { /* usage is optional */ }
120
- }
121
- const generationMs = Date.now() - start;
122
- if (streamErrors.length > 0) {
123
- adapter.recordFailure(streamErrors[0]);
124
- writeTextChunk(writer, t('ai.error.generic', lang), 'error');
125
- writeFinish(writer, 'error');
126
- return { success: true, responseText: text, reasoningText, tokenUsage, generationMs };
127
- }
128
- if (text.length > 0) {
129
- adapter.recordSuccess();
130
- writeFinish(writer);
131
- return { success: true, responseText: text, reasoningText, tokenUsage, generationMs };
132
- }
133
- writeTextChunk(writer, t('ai.error.noOutput', lang), 'no-output');
134
- writeFinish(writer);
135
- return { success: true, responseText: '', reasoningText, tokenUsage, generationMs };
136
- }
137
- catch (err) {
138
- adapter.recordFailure(err instanceof Error ? err : new Error(String(err)));
139
- console.error('[stream-helpers] Provider threw:', err.message);
140
- return { success: false, responseText: '', generationMs: Date.now() - start };
141
- }
142
- }
143
- // ── Mock Fallback ─────────────────────────────────────────
144
- export async function streamMockFallback(writer, question, lang) {
145
- const { getMockResponse } = await import('../providers/mock.js');
146
- const mockText = getMockResponse(question, lang);
147
- writer.write({
148
- type: 'message-metadata',
149
- messageMetadata: createChatStatusData({
150
- stage: 'answer',
151
- message: t('ai.status.fallback', lang),
152
- progress: 80,
153
- }),
154
- });
155
- writeTextChunk(writer, mockText, 'fallback');
156
- writeFinish(writer);
157
- return mockText;
158
- }
159
- // ── Cached Response Playback ──────────────────────────────
160
- export async function streamCachedResponse(writer, cachedResponse, config, lang) {
161
- writeSearchStatus(writer, cachedResponse.articles.length + cachedResponse.projects.length, lang);
162
- writeGeneratingStatus(writer, lang);
163
- writeSourceArticles(writer, cachedResponse.articles);
164
- writeGeneratingStatus(writer, lang, 70);
165
- const playbackGenerator = createResponsePlaybackGenerator(cachedResponse, config);
166
- let thinkingId;
167
- const textId = `text-${Date.now()}`;
168
- let textStarted = false;
169
- for await (const chunk of playbackGenerator) {
170
- if (chunk.type === 'thinking') {
171
- if (!thinkingId) {
172
- thinkingId = `thinking-${Date.now()}`;
173
- writer.write({ type: 'reasoning-start', id: thinkingId });
174
- }
175
- writer.write({ type: 'reasoning-delta', id: thinkingId, delta: chunk.text });
176
- }
177
- else {
178
- if (thinkingId) {
179
- writer.write({ type: 'reasoning-end', id: thinkingId });
180
- thinkingId = undefined;
181
- }
182
- if (!textStarted) {
183
- writer.write({ type: 'text-start', id: textId });
184
- textStarted = true;
185
- }
186
- writer.write({ type: 'text-delta', id: textId, delta: chunk.text });
187
- }
188
- }
189
- if (thinkingId) {
190
- writer.write({ type: 'reasoning-end', id: thinkingId });
191
- }
192
- if (textStarted) {
193
- writer.write({ type: 'text-end', id: textId });
194
- }
195
- writeDoneStatus(writer, lang);
196
- writeFinish(writer);
197
- }
@@ -1,13 +0,0 @@
1
- export function createChatStatusData(partial) {
2
- return {
3
- ...partial,
4
- done: partial.done ?? partial.stage === 'complete',
5
- at: Date.now(),
6
- };
7
- }
8
- export function isChatStatusData(value) {
9
- if (!value || typeof value !== 'object')
10
- return false;
11
- const v = value;
12
- return typeof v.stage === 'string' && typeof v.message === 'string' && typeof v.progress === 'number';
13
- }
@@ -1,27 +0,0 @@
1
- import { getMockResponse } from '../providers/mock.js';
2
- /**
3
- * Streams a mock response character-by-character as a ReadableStream<string>.
4
- * Simulates natural typing speed with variable delays.
5
- */
6
- export function streamMockResponse(options) {
7
- const { question, lang = 'zh', delayRange = [12, 35] } = options;
8
- const text = getMockResponse(question, lang);
9
- const [minDelay, maxDelay] = delayRange;
10
- let index = 0;
11
- return new ReadableStream({
12
- async pull(controller) {
13
- if (index >= text.length) {
14
- controller.close();
15
- return;
16
- }
17
- const chunkSize = Math.random() < 0.25 ? 2 : 1;
18
- const chunk = text.slice(index, index + chunkSize);
19
- index += chunkSize;
20
- controller.enqueue(chunk);
21
- await sleep(minDelay + Math.random() * (maxDelay - minDelay));
22
- },
23
- });
24
- }
25
- function sleep(ms) {
26
- return new Promise(resolve => setTimeout(resolve, ms));
27
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * Common CORS + SSE headers for chat API responses.
3
- */
4
- export const STREAM_HEADERS = {
5
- 'Content-Type': 'text/event-stream',
6
- 'Cache-Control': 'no-cache',
7
- 'Connection': 'keep-alive',
8
- 'Access-Control-Allow-Origin': '*',
9
- };
10
- export const JSON_HEADERS = {
11
- 'Content-Type': 'application/json',
12
- 'Access-Control-Allow-Origin': '*',
13
- };
14
- /**
15
- * Creates a standard error JSON response.
16
- */
17
- export function errorResponse(message, status = 500) {
18
- return new Response(JSON.stringify({ error: message }), {
19
- status,
20
- headers: JSON_HEADERS,
21
- });
22
- }
@@ -1,164 +0,0 @@
1
- /**
2
- * AI Package Internationalization
3
- * Follows the same pattern as packages/core/src/utils/i18n.ts
4
- */
5
- const translations = {
6
- en: {
7
- // Reasoning UI
8
- "ai.reasoning.thinking": "Thinking...",
9
- "ai.reasoning.viewReasoning": "View reasoning",
10
- "ai.reasoning.waiting": "Waiting for thoughts...",
11
- // Error messages
12
- "ai.error.network": "Network connection failed. Please check your connection.",
13
- "ai.error.aborted": "Request was cancelled.",
14
- "ai.error.rateLimit": "Too many requests. Please try again later.",
15
- "ai.error.unavailable": "AI service is temporarily unavailable.",
16
- "ai.error.generic": "Something went wrong. Please try again later.",
17
- "ai.error.format": "Invalid request format.",
18
- // UI labels
19
- "ai.placeholder": "Ask a question...",
20
- "ai.clear": "Clear",
21
- "ai.clearConversation": "Clear conversation",
22
- "ai.close": "Close",
23
- "ai.closeChat": "Close chat",
24
- "ai.retry": "Retry",
25
- "ai.status.searching": "Searching...",
26
- "ai.status.generating": "Generating response...",
27
- "ai.status.found": "Found {count} related items",
28
- "ai.status.citation": "Answered from public records",
29
- "ai.status.fallback": "AI service unavailable, using demo mode",
30
- // Quick prompts
31
- "ai.prompt.techStack": "What tech stack is used?",
32
- "ai.prompt.recommend": "Recommend some articles?",
33
- "ai.prompt.build": "How to build a similar blog?",
34
- "ai.prompt.summarize": 'Summarize the key points of "{title}"',
35
- "ai.prompt.explain": 'Explain "{point}"',
36
- "ai.prompt.related": "What related content should I read next?",
37
- // Welcome messages
38
- "ai.welcome.reading": 'I\'m reading "{title}" with you.\nAsk me to summarize, explain a concept, or explore related topics.',
39
- "ai.welcome.canHelp": "Hi! I'm the blog AI assistant. Ask me anything and I'll help you find related articles.",
40
- "ai.welcome.greeting": "Hi! I'm the blog AI assistant.",
41
- "ai.welcome.demo": "I'm running in demo mode. I can recommend blog articles and external resources.",
42
- "ai.welcome.demoHint": "For full AI features (RAG search), configure AI_BASE_URL and AI_API_KEY.",
43
- "ai.welcome.demoPrompt": 'Try: "Recommend articles?" or "How to build this blog?"',
44
- // Header
45
- "ai.header.reading": "Reading:",
46
- "ai.header.mode": "Demo",
47
- // Assistant branding
48
- "ai.assistantName": "Blog Avatar",
49
- "ai.status.live": "Live",
50
- // Additional error messages
51
- "ai.error.emptyMessage": "Message cannot be empty.",
52
- "ai.error.emptyContent": "Message content cannot be empty.",
53
- "ai.error.inputTooLong": "Message too long, max {max} characters.",
54
- "ai.error.timeout": "Response timeout, please retry or simplify your question.",
55
- // Rate limit messages
56
- "ai.error.rateLimit.burst": "Too many requests, please try again later.",
57
- "ai.error.rateLimit.sustained": "Too many requests, please wait a minute.",
58
- "ai.error.rateLimit.daily": "Daily limit reached, please come back tomorrow.",
59
- "ai.error.noOutput": "Sorry, I could not generate a valid response. Please try rephrasing your question.",
60
- "ai.prompt.section.responsibilities": "Your Responsibilities",
61
- "ai.prompt.section.format": "Response Format",
62
- "ai.prompt.section.principles": "Recommendation Principles",
63
- "ai.prompt.section.constraints": "Constraints",
64
- "ai.prompt.section.sourceLayers": "Source Priority Protocol (must follow)",
65
- "ai.prompt.section.privacy": "Privacy Protection",
66
- "ai.prompt.section.answerModes": "Answer Mode Guide (follow detected mode)",
67
- "ai.prompt.section.preOutputChecks": "Pre-Output Checks (execute mentally, do not output steps)",
68
- "ai.semiStatic.blogOverview": "Blog Overview",
69
- "ai.semiStatic.totalPosts": "{count} posts total",
70
- "ai.semiStatic.mainCategories": "Main categories: {categories}",
71
- "ai.semiStatic.latestArticles": "Latest Posts",
72
- },
73
- zh: {
74
- // Reasoning UI
75
- "ai.reasoning.thinking": "思考中...",
76
- "ai.reasoning.viewReasoning": "查看思考过程",
77
- "ai.reasoning.waiting": "等待思考...",
78
- // Error messages
79
- "ai.error.network": "网络连接失败,请检查网络",
80
- "ai.error.aborted": "请求已取消",
81
- "ai.error.rateLimit": "请求太频繁,请稍后再试",
82
- "ai.error.unavailable": "AI 服务暂时不可用",
83
- "ai.error.generic": "出了点问题,请稍后再试",
84
- "ai.error.format": "请求格式错误",
85
- // UI labels
86
- "ai.placeholder": "输入你的问题...",
87
- "ai.clear": "清除",
88
- "ai.clearConversation": "清除对话",
89
- "ai.close": "关闭",
90
- "ai.closeChat": "关闭聊天",
91
- "ai.retry": "重试",
92
- "ai.status.searching": "搜索中...",
93
- "ai.status.generating": "正在生成回答...",
94
- "ai.status.found": "找到 {count} 篇相关内容",
95
- "ai.status.citation": "已基于公开记录直接给出回答",
96
- "ai.status.fallback": "AI 服务不可用,使用演示模式回复",
97
- // Quick prompts
98
- "ai.prompt.techStack": "这个博客用了什么技术?",
99
- "ai.prompt.recommend": "有哪些文章推荐?",
100
- "ai.prompt.build": "怎么搭建类似的博客?",
101
- "ai.prompt.summarize": "总结一下《{title}》的核心观点",
102
- "ai.prompt.explain": "解释一下「{point}」",
103
- "ai.prompt.related": "这篇文章和哪些内容相关?",
104
- // Welcome messages
105
- "ai.welcome.reading": "我在结合《{title}》陪你阅读。\n你可以让我总结这篇文章、解释某个观点,或者顺着这篇文章继续延伸到相关主题。",
106
- "ai.welcome.canHelp": "你好!我是博客 AI 助手,问我任何关于博客内容的问题,我可以帮你找到相关文章。",
107
- "ai.welcome.greeting": "你好!我是博客 AI 助手。",
108
- "ai.welcome.demo": "我目前在 Demo 模式下,可以推荐博客文章和外部资源。",
109
- "ai.welcome.demoHint": "启用完整 AI 功能(RAG 搜索增强)需要配置 AI_BASE_URL 和 AI_API_KEY 环境变量。",
110
- "ai.welcome.demoPrompt": "试试:「有哪些文章推荐?」或「怎么搭建类似的博客?」",
111
- // Header
112
- "ai.header.reading": "正在阅读:",
113
- "ai.header.mode": "演示",
114
- // Assistant branding
115
- "ai.assistantName": "博客分身",
116
- "ai.status.live": "在线",
117
- // Additional error messages
118
- "ai.error.emptyMessage": "消息不能为空。",
119
- "ai.error.emptyContent": "消息内容不能为空。",
120
- "ai.error.inputTooLong": "消息过长,最多 {max} 字。",
121
- "ai.error.timeout": "响应超时,请重试或简化问题。",
122
- // Rate limit messages
123
- "ai.error.rateLimit.burst": "请求太频繁,请稍后再试。",
124
- "ai.error.rateLimit.sustained": "请求次数过多,请一分钟后再试。",
125
- "ai.error.rateLimit.daily": "今日对话次数已达上限,请明天再来。",
126
- "ai.error.noOutput": "抱歉,我无法生成有效的回答。请尝试换一种方式提问。",
127
- "ai.prompt.section.responsibilities": "你的职责",
128
- "ai.prompt.section.format": "回答格式",
129
- "ai.prompt.section.principles": "推荐原则",
130
- "ai.prompt.section.constraints": "约束",
131
- "ai.prompt.section.sourceLayers": "来源分层协议(必须遵守)",
132
- "ai.prompt.section.privacy": "隐私保护",
133
- "ai.prompt.section.answerModes": "回答模式指导(按检测到的模式执行)",
134
- "ai.prompt.section.preOutputChecks": "输出前检查(在心里执行,不输出步骤)",
135
- "ai.semiStatic.blogOverview": "博客概况",
136
- "ai.semiStatic.totalPosts": "共有 {count} 篇文章",
137
- "ai.semiStatic.mainCategories": "主要分类:{categories}",
138
- "ai.semiStatic.latestArticles": "最新文章",
139
- },
140
- };
141
- /**
142
- * Get translation by key.
143
- * @param key - Translation key (type-safe)
144
- * @param lang - Language code ('zh' or 'en')
145
- * @param vars - Optional variables for interpolation (e.g., { count: 5 })
146
- */
147
- export function t(key, lang = 'zh', vars) {
148
- const l = lang === 'zh' ? 'zh' : 'en';
149
- let text = translations[l]?.[key] ?? translations['en'][key] ?? key;
150
- // Interpolate variables like {count}, {title}, etc.
151
- if (vars) {
152
- for (const [k, v] of Object.entries(vars)) {
153
- text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
154
- }
155
- }
156
- return text;
157
- }
158
- /**
159
- * Get normalized language code.
160
- * Returns 'zh' for Chinese, 'en' for everything else.
161
- */
162
- export function getLang(lang) {
163
- return lang === 'zh' ? 'zh' : 'en';
164
- }
@@ -1,31 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import { useState, useCallback } from 'preact/hooks';
3
- import { ChatPanel } from './ChatPanel.tsx';
4
- import type { AIChatConfig } from './ChatPanel.tsx';
5
- import type { ArticleChatContext } from '../server/types.ts';
6
-
7
- interface Props {
8
- config: AIChatConfig;
9
- articleContext?: ArticleChatContext;
10
- }
11
-
12
- export default function AIChatContainer({ config, articleContext }: Props) {
13
- const [open, setOpen] = useState(false);
14
-
15
- const handleToggle = useCallback(() => setOpen(prev => !prev), []);
16
- const handleClose = useCallback(() => setOpen(false), []);
17
-
18
- if (typeof window !== 'undefined') {
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- (window as any).__aiChatToggle = handleToggle;
21
- }
22
-
23
- return (
24
- <ChatPanel
25
- open={open}
26
- onClose={handleClose}
27
- config={config}
28
- articleContext={articleContext}
29
- />
30
- );
31
- }