mr4 commited on
Commit
91111e4
·
verified ·
1 Parent(s): 065017f

Upload 26 files

Browse files
adapters/adapter-registry.js CHANGED
@@ -1,61 +1,69 @@
1
- // ============================================================
2
- // Adapter Registry — Metadata, display names, và đăng ký adapter
3
- // Phụ thuộc: currentLang, ADAPTER_REGISTRY (từ data-loader.js / globalThis),
4
- // bestMatchAdapter, logicAdapterDispatcher, mathematicalEvaluationAdapter,
5
- // specificResponseAdapter, timeAdapter, unitConversionAdapter
6
- // ============================================================
7
-
8
- var ADAPTER_DISPLAY_NAMES = {
9
- specific_response: { vi: 'Phản hồi cụ thể', en: 'Specific Response', ja: '特定応答' },
10
- time_adapter: { vi: 'Thời gian', en: 'Time', ja: '時間' },
11
- mathematical_evaluation: { vi: 'Tính toán', en: 'Math', ja: '数学計算' },
12
- unit_conversion: { vi: 'Chuyển đổi đơn vị', en: 'Unit Conversion', ja: '単位変換' },
13
- best_match: { vi: 'Best Match', en: 'Best Match', ja: 'ベストマッチ' },
14
- logic_adapter: { vi: 'Logic Adapter', en: 'Logic Adapter', ja: 'ロジックアダプター' },
15
- rivescript: { vi: 'RiveScript', en: 'RiveScript', ja: 'RiveScript' },
16
- fallback_api: { vi: 'Fallback API', en: 'Fallback API', ja: 'Fallback API' },
17
- web_search: { vi: 'Tìm kiếm Web', en: 'Web Search', ja: 'ウェブ検索' },
18
- llm_adapter: { vi: 'LLM (WebGPU)', en: 'LLM (WebGPU)', ja: 'LLM(WebGPU)' }
19
- };
20
-
21
- function getAdapterDisplayName(adapterKey) {
22
- var lang = currentLang || 'vi';
23
- var names = ADAPTER_DISPLAY_NAMES[adapterKey];
24
- if (!names) return adapterKey;
25
- return names[lang] || names['vi'] || adapterKey;
26
- }
27
-
28
- function registerAdapters(bot, lang) {
29
- var ADAPTER_FUNCTIONS = {
30
- best_match: bestMatchAdapter,
31
- logic_adapter: logicAdapterDispatcher,
32
- mathematical_evaluation: mathematicalEvaluationAdapter,
33
- specific_response: specificResponseAdapter,
34
- time_adapter: timeAdapter,
35
- unit_conversion: unitConversionAdapter,
36
- web_search: webSearchAdapter,
37
- llm_adapter: llmAdapter
38
- };
39
-
40
- var adapterNames = Object.keys(ADAPTER_REGISTRY);
41
- for (var i = 0; i < adapterNames.length; i++) {
42
- var name = adapterNames[i];
43
- var entry = ADAPTER_REGISTRY[name];
44
- var fn = ADAPTER_FUNCTIONS[name];
45
-
46
- if (entry.active && typeof fn === 'function') {
47
- (function (adapterFn) {
48
- bot.setSubroutine(name, function (rs, args) {
49
- return adapterFn(rs, args);
50
- });
51
- })(fn);
52
- }
53
- }
54
- }
55
-
56
- // Node/test: export to globalThis
57
- if (typeof module !== 'undefined' && module.exports) {
58
- globalThis.ADAPTER_DISPLAY_NAMES = ADAPTER_DISPLAY_NAMES;
59
- globalThis.getAdapterDisplayName = getAdapterDisplayName;
60
- globalThis.registerAdapters = registerAdapters;
61
- }
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // Adapter Registry — Metadata, display names, và đăng ký adapter
3
+ // Phụ thuộc: currentLang, ADAPTER_REGISTRY (từ data-loader.js / globalThis),
4
+ // bestMatchAdapter, logicAdapterDispatcher, mathematicalEvaluationAdapter,
5
+ // specificResponseAdapter, timeAdapter, unitConversionAdapter
6
+ // ============================================================
7
+
8
+ var ADAPTER_DISPLAY_NAMES = {
9
+ specific_response: { vi: 'Phản hồi cụ thể', en: 'Specific Response', ja: '特定応答' },
10
+ time_adapter: { vi: 'Thời gian', en: 'Time', ja: '時間' },
11
+ mathematical_evaluation: { vi: 'Tính toán', en: 'Math', ja: '数学計算' },
12
+ unit_conversion: { vi: 'Chuyển đổi đơn vị', en: 'Unit Conversion', ja: '単位変換' },
13
+ best_match: { vi: 'Best Match', en: 'Best Match', ja: 'ベストマッチ' },
14
+ logic_adapter: { vi: 'Logic Adapter', en: 'Logic Adapter', ja: 'ロジックアダプター' },
15
+ rivescript: { vi: 'RiveScript', en: 'RiveScript', ja: 'RiveScript' },
16
+ fallback_api: { vi: 'Fallback API', en: 'Fallback API', ja: 'Fallback API' },
17
+ web_search: { vi: 'Tìm kiếm Web', en: 'Web Search', ja: 'ウェブ検索' },
18
+ llm_adapter: { vi: 'LLM (WebGPU)', en: 'LLM (WebGPU)', ja: 'LLM(WebGPU)' }
19
+ };
20
+
21
+ function getAdapterDisplayName(adapterKey) {
22
+ var lang = currentLang || 'vi';
23
+ var names = ADAPTER_DISPLAY_NAMES[adapterKey];
24
+ if (!names) return adapterKey;
25
+ return names[lang] || names['vi'] || adapterKey;
26
+ }
27
+
28
+ function registerAdapters(bot, lang) {
29
+ var ADAPTER_FUNCTIONS = {
30
+ best_match: bestMatchAdapter,
31
+ logic_adapter: logicAdapterDispatcher,
32
+ mathematical_evaluation: mathematicalEvaluationAdapter,
33
+ specific_response: specificResponseAdapter,
34
+ time_adapter: timeAdapter,
35
+ unit_conversion: unitConversionAdapter,
36
+ web_search: webSearchAdapter,
37
+ llm_adapter: llmAdapter
38
+ };
39
+
40
+ var adapterNames = Object.keys(ADAPTER_REGISTRY);
41
+ for (var i = 0; i < adapterNames.length; i++) {
42
+ var name = adapterNames[i];
43
+ var entry = ADAPTER_REGISTRY[name];
44
+ var fn = ADAPTER_FUNCTIONS[name];
45
+
46
+ if (typeof fn === 'function') {
47
+ // Luôn đăng ký adapter, nhưng wrapper kiểm tra active flag
48
+ (function (adapterKey, adapterFn) {
49
+ bot.setSubroutine(adapterKey, function (rs, args) {
50
+ // Kiểm tra active flag trước khi gọi adapter
51
+ if (ADAPTER_REGISTRY[adapterKey] && ADAPTER_REGISTRY[adapterKey].active === false) {
52
+ // Adapter bị disabled, trả về thông báo
53
+ if (lang === 'en') return 'This adapter is currently disabled.';
54
+ if (lang === 'ja') return 'このアダプターは現在無効です。';
55
+ return 'Adapter này hiện đang bị vô hiệu hóa.';
56
+ }
57
+ return adapterFn(rs, args);
58
+ });
59
+ })(name, fn);
60
+ }
61
+ }
62
+ }
63
+
64
+ // Node/test: export to globalThis
65
+ if (typeof module !== 'undefined' && module.exports) {
66
+ globalThis.ADAPTER_DISPLAY_NAMES = ADAPTER_DISPLAY_NAMES;
67
+ globalThis.getAdapterDisplayName = getAdapterDisplayName;
68
+ globalThis.registerAdapters = registerAdapters;
69
+ }
adapters/best-match.js CHANGED
@@ -1,58 +1,58 @@
1
- // ============================================================
2
- // Best Match Adapter — So khớp tương đồng chuỗi với tập Q&A
3
- // Phụ thuộc: currentLang, _adapterPath, textSimilarity, removeVietnameseDiacritics,
4
- // QA_DATASET, findBestMatchPreprocessed (optional)
5
- // ============================================================
6
-
7
- function bestMatchAdapter(rs, args) {
8
- _adapterPath.push('best_match');
9
- var input = (args || []).join(' ').trim();
10
- var lang = currentLang || 'vi';
11
- var threshold = 0.55;
12
-
13
- if (input.length === 0) {
14
- if (lang === 'en') return { answer: 'Please provide a question to search for.', score: 0 };
15
- if (lang === 'ja') return { answer: '検索する質問を入力してください。', score: 0 };
16
- return { answer: 'Vui lòng nhập câu hỏi để tìm kiếm.', score: 0 };
17
- }
18
-
19
- // Thử dùng preprocessed data trước (nhanh hơn)
20
- if (typeof findBestMatchPreprocessed === 'function') {
21
- var ppResult = findBestMatchPreprocessed(input, lang, threshold);
22
- if (ppResult) return { answer: ppResult.answer, score: ppResult.score || threshold };
23
- }
24
-
25
- // Fallback: tính similarity thủ công từ QA_DATASET
26
- var dataset = QA_DATASET[lang] || QA_DATASET['vi'];
27
- var bestScore = -1;
28
- var bestAnswer = '';
29
-
30
- var inputLower = input.toLowerCase();
31
- var inputNorm = (lang === 'vi' && typeof removeVietnameseDiacritics === 'function')
32
- ? removeVietnameseDiacritics(inputLower) : null;
33
-
34
- for (var i = 0; i < dataset.length; i++) {
35
- var qLower = dataset[i].q.toLowerCase();
36
- var score = textSimilarity(inputLower, qLower);
37
- if (inputNorm) {
38
- var qNorm = removeVietnameseDiacritics(qLower);
39
- var scoreNorm = textSimilarity(inputNorm, qNorm);
40
- if (scoreNorm > score) score = scoreNorm;
41
- }
42
- if (score > bestScore) {
43
- bestScore = score;
44
- bestAnswer = dataset[i].a;
45
- }
46
- }
47
-
48
- if (bestScore >= threshold) return { answer: bestAnswer, score: bestScore };
49
-
50
- if (lang === 'en') return { answer: 'Sorry, I could not find a suitable answer for your question.', score: bestScore };
51
- if (lang === 'ja') return { answer: '申し訳ありませんがご質問に適した回答が見つかりませんでした。', score: bestScore };
52
- return { answer: 'Xin lỗi, mình không tìm được câu trả lời phù hợp cho câu hỏi của bạn.', score: bestScore };
53
- }
54
-
55
- // Node/test: export to globalThis
56
- if (typeof module !== 'undefined' && module.exports) {
57
- globalThis.bestMatchAdapter = bestMatchAdapter;
58
- }
 
1
+ // ============================================================
2
+ // Best Match Adapter — So khớp tương đồng chuỗi với tập Q&A
3
+ // Phụ thuộc: currentLang, _adapterPath, textSimilarity, removeVietnameseDiacritics,
4
+ // QA_DATASET, findBestMatchPreprocessed (optional)
5
+ // ============================================================
6
+
7
+ function bestMatchAdapter(rs, args) {
8
+ _adapterPath.push('best_match');
9
+ var input = (args || []).join(' ').trim();
10
+ var lang = currentLang || 'vi';
11
+ var threshold = 0.55;
12
+
13
+ if (input.length === 0) {
14
+ if (lang === 'en') return { answer: 'Please provide a question to search for.', score: 0 };
15
+ if (lang === 'ja') return { answer: '検索する質問を入力してください。', score: 0 };
16
+ return { answer: 'Vui lòng nhập câu hỏi để tìm kiếm.', score: 0 };
17
+ }
18
+
19
+ // Thử dùng preprocessed data trước (nhanh hơn)
20
+ if (typeof findBestMatchPreprocessed === 'function') {
21
+ var ppResult = findBestMatchPreprocessed(input, lang, threshold);
22
+ if (ppResult) return { answer: ppResult.answer, score: ppResult.score || threshold };
23
+ }
24
+
25
+ // Fallback: tính similarity thủ công từ QA_DATASET
26
+ var dataset = QA_DATASET[lang] || QA_DATASET['vi'];
27
+ var bestScore = -1;
28
+ var bestAnswer = '';
29
+
30
+ var inputLower = input.toLowerCase();
31
+ var inputNorm = (lang === 'vi' && typeof removeVietnameseDiacritics === 'function')
32
+ ? removeVietnameseDiacritics(inputLower) : null;
33
+
34
+ for (var i = 0; i < dataset.length; i++) {
35
+ var qLower = dataset[i].q.toLowerCase();
36
+ var score = textSimilarity(inputLower, qLower);
37
+ if (inputNorm) {
38
+ var qNorm = removeVietnameseDiacritics(qLower);
39
+ var scoreNorm = textSimilarity(inputNorm, qNorm);
40
+ if (scoreNorm > score) score = scoreNorm;
41
+ }
42
+ if (score > bestScore) {
43
+ bestScore = score;
44
+ bestAnswer = dataset[i].a;
45
+ }
46
+ }
47
+
48
+ if (bestScore >= threshold) return { answer: bestAnswer, score: bestScore };
49
+
50
+ if (lang === 'en') return { answer: 'Sorry, I could not find a suitable answer for your question.', score: bestScore };
51
+ if (lang === 'ja') return { answer: '申し訳ありませんが���ご質問に適した回答が見つかりませんでした。', score: bestScore };
52
+ return { answer: 'Xin lỗi, mình không tìm được câu trả lời phù hợp cho câu hỏi của bạn.', score: bestScore };
53
+ }
54
+
55
+ // Node/test: export to globalThis
56
+ if (typeof module !== 'undefined' && module.exports) {
57
+ globalThis.bestMatchAdapter = bestMatchAdapter;
58
+ }
adapters/llm-adapter.js CHANGED
@@ -1,460 +1,461 @@
1
- // ============================================================
2
- // LLM Adapter — Chạy LLM trực tiếp trên trình duyệt qua WebGPU
3
- // Tham khảo: webml-community/Qwen3.5-WebGPU (HuggingFace Spaces)
4
- // Sử dụng @huggingface/transformers để load model và generate text
5
- // Phụ thuộc: currentLang, _adapterPath
6
- // ============================================================
7
-
8
- /**
9
- * Trạng thái LLM — singleton, load model 1 lần duy nhất.
10
- */
11
- var _llmProcessor = null;
12
- var _llmModel = null;
13
- var _llmLoading = false;
14
- var _llmReady = false;
15
- var _llmLoadError = null;
16
- var _llmTransformers = null; // Lưu ref module transformers để dùng RawImage
17
- var _llmLastError = null; // Lỗi cuối cùng từ LLM (để hiển thị cho user debug)
18
- var _llmStoppingCriteria = null; // InterruptableStoppingCriteria để cancel generate
19
- var _llmGenerating = false; // Đang generate hay không
20
- var _llmThinkingEnabled = false; // Mặc định tắt thinking
21
-
22
- /**
23
- * Callback để thông báo trạng thái loading lên UI.
24
- * Được set từ app.js qua setLLMStatusCallback().
25
- * @type {function(string, string)|null}
26
- * - action: 'loading_start' | 'loading_progress' | 'loading_done' | 'loading_error'
27
- * - message: Mô tả trạng thái
28
- */
29
- var _llmStatusCallback = null;
30
-
31
- /**
32
- * Đăng ký callback nhận thông báo trạng thái loading.
33
- * @param {function(string, string)} callback - fn(action, message)
34
- */
35
- function setLLMStatusCallback(callback) {
36
- _llmStatusCallback = typeof callback === 'function' ? callback : null;
37
- }
38
-
39
- function _notifyStatus(action, message) {
40
- if (typeof _llmStatusCallback === 'function') {
41
- try { _llmStatusCallback(action, message); } catch (e) { /* ignore */ }
42
- }
43
- }
44
-
45
- /**
46
- * Model ID mặc định — Qwen3.5 0.6B (nhỏ nhất, phù hợp chạy trên browser).
47
- * Có thể thay đổi bằng cách gọi setLLMModelId() trước khi load.
48
- */
49
- var LLM_MODEL_ID = 'onnx-community/Qwen3.5-0.8B-ONNX-OPT';
50
-
51
- /**
52
- * Max tokens cho mỗi lần generate.
53
- */
54
- var LLM_MAX_NEW_TOKENS = 256;
55
-
56
- /**
57
- * Thay đổi model ID (phải gọi trước loadLLMModel).
58
- * @param {string} modelId - HuggingFace model ID
59
- */
60
- function setLLMModelId(modelId) {
61
- if (typeof modelId === 'string' && modelId.trim()) {
62
- LLM_MODEL_ID = modelId.trim();
63
- }
64
- }
65
-
66
- /**
67
- * Bật/tắt thinking mode.
68
- * @param {boolean} enabled
69
- */
70
- function setLLMThinkingEnabled(enabled) {
71
- _llmThinkingEnabled = !!enabled;
72
- }
73
-
74
- /**
75
- * Kiểm tra thinking mode có đang bật không.
76
- * @returns {boolean}
77
- */
78
- function isLLMThinkingEnabled() {
79
- return _llmThinkingEnabled;
80
- }
81
-
82
- /**
83
- * Build prompt string từ history + message hiện tại.
84
- * History được truyền từ app.js, đã trim sẵn.
85
- * @param {string} userMessage
86
- * @param {boolean} hasImage
87
- * @param {Array<{role: string, content: string}>} [history] - Lịch sử hội thoại đã trim
88
- * @returns {string}
89
- */
90
- function _buildPromptWithHistory(userMessage, hasImage, history) {
91
- var prompt = '';
92
-
93
- // Thêm history nếu có
94
- if (history && history.length > 0) {
95
- for (var i = 0; i < history.length; i++) {
96
- var msg = history[i];
97
- prompt += '<|im_start|>' + msg.role + '\n' + msg.content + '<|im_end|>\n';
98
- }
99
- }
100
-
101
- // Thêm message hiện tại
102
- prompt += '<|im_start|>user\n';
103
- if (hasImage) {
104
- prompt += '<|vision_start|><|image_pad|><|vision_end|>';
105
- }
106
- prompt += userMessage + '<|im_end|>\n';
107
-
108
- // Assistant prefix
109
- prompt += '<|im_start|>assistant\n';
110
- if (_llmThinkingEnabled) {
111
- prompt += '<think>\n';
112
- }
113
-
114
- return prompt;
115
- }
116
-
117
- /**
118
- * Kiểm tra trình duyệt có hỗ trợ WebGPU không.
119
- * @returns {boolean}
120
- */
121
- function isWebGPUSupported() {
122
- return typeof navigator !== 'undefined' && !!navigator.gpu;
123
- }
124
-
125
- /**
126
- * Load model và processor (lazy — chỉ load khi cần lần đầu).
127
- * Tham khảo cách Qwen3.5-WebGPU load model với dtype q4 device webgpu.
128
- * @returns {Promise<boolean>} true nếu load thành công
129
- */
130
- async function loadLLMModel() {
131
- if (_llmReady) return true;
132
- if (_llmLoadError) return false;
133
- if (_llmLoading) {
134
- // Đợi nếu đang load
135
- return new Promise(function (resolve) {
136
- var check = setInterval(function () {
137
- if (!_llmLoading) {
138
- clearInterval(check);
139
- resolve(_llmReady);
140
- }
141
- }, 200);
142
- });
143
- }
144
-
145
- _llmLoading = true;
146
-
147
- try {
148
- // Dynamic import — @huggingface/transformers từ CDN
149
- var transformers;
150
- if (typeof module !== 'undefined' && module.exports) {
151
- // Node/test: skip WebGPU không khả dụng
152
- _llmLoadError = 'WebGPU not available in Node.js';
153
- _llmLoading = false;
154
- return false;
155
- }
156
-
157
- // Browser: import từ CDN (giống Qwen3.5-WebGPU)
158
- transformers = await import(
159
- 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0'
160
- );
161
- _llmTransformers = transformers;
162
-
163
- // Khởi tạo stopping criteria để hỗ trợ cancel
164
- _llmStoppingCriteria = new transformers.InterruptableStoppingCriteria();
165
-
166
- if (!isWebGPUSupported()) {
167
- _llmLoadError = 'WebGPU not supported';
168
- _llmLoading = false;
169
- return false;
170
- }
171
-
172
- console.log('[LLM Adapter] Loading processor:', LLM_MODEL_ID);
173
- _notifyStatus('loading_start', 'Loading processor...');
174
- _llmProcessor = await transformers.AutoProcessor.from_pretrained(LLM_MODEL_ID);
175
-
176
- console.log('[LLM Adapter] Loading model:', LLM_MODEL_ID);
177
- _notifyStatus('loading_progress', 'Loading model weights...');
178
- _llmModel = await transformers.Qwen3_5ForConditionalGeneration.from_pretrained(
179
- LLM_MODEL_ID,
180
- {
181
- dtype: {
182
- embed_tokens: 'q4',
183
- vision_encoder: 'fp16',
184
- decoder_model_merged: 'q4'
185
- },
186
- device: 'webgpu'
187
- }
188
- );
189
-
190
- _llmReady = true;
191
- _llmLoading = false;
192
- console.log('[LLM Adapter] Model loaded successfully');
193
- _notifyStatus('loading_done', 'Model loaded successfully');
194
- return true;
195
- } catch (err) {
196
- console.error('[LLM Adapter] Failed to load model:', err);
197
- _llmLoadError = err.message || 'Unknown error';
198
- _llmLoading = false;
199
- _notifyStatus('loading_error', 'Failed to load model: ' + _llmLoadError);
200
- return false;
201
- }
202
- }
203
-
204
- /**
205
- * Helper: dispose past_key_values để giải phóng bộ nhớ GPU.
206
- */
207
- function _disposePastKeyValues(pastKeyValues) {
208
- if (!pastKeyValues) return;
209
- try {
210
- for (var key of Object.keys(pastKeyValues)) {
211
- if (pastKeyValues[key] && typeof pastKeyValues[key].dispose === 'function') {
212
- pastKeyValues[key].dispose();
213
- }
214
- }
215
- } catch (e) { /* ignore */ }
216
- }
217
-
218
- /**
219
- * Generate text từ LLM cho một prompt.
220
- * Sử dụng TextStreamer để thu thập response token-by-token.
221
- * @param {string} userMessage - Tin nhắn người dùng
222
- * @param {function} [onToken] - Callback nhận text tích lũy mỗi khi có token mới: fn(accumulatedText)
223
- * @param {Array} [history] - Lịch sử hội thoại đã trim
224
- * @returns {Promise<string|null>} Phản hồi từ LLM hoặc null nếu lỗi
225
- */
226
- async function llmGenerate(userMessage, onToken, history) {
227
- if (!_llmReady || !_llmModel || !_llmProcessor || !_llmTransformers) return null;
228
-
229
- _llmGenerating = true;
230
- try {
231
- var prompt = _buildPromptWithHistory(userMessage, false, history);
232
-
233
- var inputs = await _llmProcessor(prompt);
234
-
235
- var fullText = '';
236
- var streamer = new _llmTransformers.TextStreamer(_llmProcessor.tokenizer, {
237
- skip_prompt: true,
238
- skip_special_tokens: true,
239
- callback_function: function (token) {
240
- fullText += token;
241
- if (typeof onToken === 'function') {
242
- onToken(fullText);
243
- }
244
- }
245
- });
246
-
247
- if (_llmStoppingCriteria) _llmStoppingCriteria.reset();
248
-
249
- var generateOpts = {
250
- ...inputs,
251
- max_new_tokens: LLM_MAX_NEW_TOKENS,
252
- do_sample: true,
253
- streamer: streamer,
254
- return_dict_in_generate: true
255
- };
256
- if (_llmStoppingCriteria) {
257
- generateOpts.stopping_criteria = _llmStoppingCriteria;
258
- }
259
-
260
- var result = await _llmModel.generate(generateOpts);
261
-
262
- _disposePastKeyValues(result.past_key_values);
263
-
264
- var trimmed = fullText.replace(/^\n+/, '').trim();
265
- _llmGenerating = false;
266
- return trimmed.length > 0 ? trimmed : null;
267
- } catch (err) {
268
- console.error('[LLM Adapter] Generate error:', err);
269
- _llmLastError = 'Generate error: ' + (err.message || err);
270
- _llmGenerating = false;
271
- return null;
272
- }
273
- }
274
-
275
- /**
276
- * Generate text từ LLM cho một prompt kèm image.
277
- * Sử dụng TextStreamer để thu thập response token-by-token.
278
- * @param {string} userMessage - Tin nhắn người dùng
279
- * @param {string} imageDataURL - Data URL hoặc blob URL của ảnh
280
- * @param {function} [onToken] - Callback nhận text tích lũy mỗi khi có token mới: fn(accumulatedText)
281
- * @param {Array} [history] - Lịch sử hội thoại đã trim
282
- * @returns {Promise<string|null>} Phản hồi từ LLM hoặc null nếu lỗi
283
- */
284
- async function llmGenerateWithImage(userMessage, imageDataURL, onToken, history) {
285
- if (!_llmReady || !_llmModel || !_llmProcessor || !_llmTransformers) return null;
286
-
287
- _llmGenerating = true;
288
- try {
289
- var rawImage = await _llmTransformers.RawImage.read(imageDataURL);
290
- var resizedImage = await rawImage.resize(448, 448);
291
-
292
- var prompt = _buildPromptWithHistory(userMessage || '', true, history);
293
-
294
- var inputs = await _llmProcessor(prompt, resizedImage);
295
-
296
- var fullText = '';
297
- var streamer = new _llmTransformers.TextStreamer(_llmProcessor.tokenizer, {
298
- skip_prompt: true,
299
- skip_special_tokens: true,
300
- callback_function: function (token) {
301
- fullText += token;
302
- if (typeof onToken === 'function') {
303
- onToken(fullText);
304
- }
305
- }
306
- });
307
-
308
- if (_llmStoppingCriteria) _llmStoppingCriteria.reset();
309
-
310
- var generateOpts = {
311
- ...inputs,
312
- max_new_tokens: LLM_MAX_NEW_TOKENS,
313
- do_sample: true,
314
- streamer: streamer,
315
- return_dict_in_generate: true
316
- };
317
- if (_llmStoppingCriteria) {
318
- generateOpts.stopping_criteria = _llmStoppingCriteria;
319
- }
320
-
321
- var result = await _llmModel.generate(generateOpts);
322
-
323
- _disposePastKeyValues(result.past_key_values);
324
-
325
- var trimmed = fullText.replace(/^\n+/, '').trim();
326
- _llmGenerating = false;
327
-
328
- return trimmed.length > 0 ? trimmed : null;
329
- } catch (err) {
330
- console.error('[LLM Adapter] Generate with image error:', err);
331
- _llmLastError = 'Generate with image error: ' + (err.message || err);
332
- _llmGenerating = false;
333
- return null;
334
- }
335
- }
336
-
337
- /**
338
- * LLM Adapter — Interface chính, tương thích với hệ thống adapter.
339
- * thể dùng như subroutine hoặc gọi trực tiếp.
340
- * @param {object} rs - RiveScript instance (có thể null)
341
- * @param {string[]} args - Mảng từ (input đã split)
342
- * @param {string} [imageDataURL] - Data URL của ảnh đính kèm (optional)
343
- * @param {function} [onToken] - Callback stream token realtime: fn(accumulatedText)
344
- * @param {Array} [history] - Lịch sử hội thoại đã trim từ app.js
345
- * @returns {Promise<string|null>} Phản hồi từ LLM hoặc null
346
- */
347
- async function llmAdapter(rs, args, imageDataURL, onToken, history) {
348
- _adapterPath.push('llm_adapter');
349
- _llmLastError = null;
350
- var query = (args || []).join(' ').trim();
351
- var lang = currentLang || 'vi';
352
-
353
- // Cho phép gửi ảnh mà không cần text (mô tả ảnh)
354
- if (query.length === 0 && !imageDataURL) {
355
- if (lang === 'en') return 'Please provide a message.';
356
- if (lang === 'ja') return 'メッセージを入力してください。';
357
- return 'Vui lòng nhập tin nhắn.';
358
- }
359
-
360
- // Nếu chỉ có ảnh mà không có text, thêm prompt mặc định
361
- if (query.length === 0 && imageDataURL) {
362
- if (lang === 'en') query = 'Describe this image in detail.';
363
- else if (lang === 'ja') query = 'この画像を詳しく説明してください。';
364
- else query = 'Hãy tả chi tiết hình ảnh này.';
365
- }
366
-
367
- // Kiểm tra WebGPU
368
- if (!isWebGPUSupported()) {
369
- _llmLastError = 'WebGPU not supported in this browser';
370
- return null;
371
- }
372
-
373
- // Load model nếu chưa load
374
- var loaded = await loadLLMModel();
375
- if (!loaded) {
376
- _llmLastError = 'Model load failed: ' + (_llmLoadError || 'unknown');
377
- return null;
378
- }
379
-
380
- // Generate — không timeout, chờ LLM chạy hết
381
- var generateFn = imageDataURL
382
- ? llmGenerateWithImage(query, imageDataURL, onToken, history)
383
- : llmGenerate(query, onToken, history);
384
-
385
- var result = await generateFn;
386
-
387
- if (result) return result;
388
-
389
- if (!_llmLastError) {
390
- _llmLastError = 'Generate returned empty response';
391
- }
392
- return null;
393
- }
394
-
395
- /**
396
- * Hủy bỏ quá trình generate đang chạy.
397
- */
398
- function cancelLLMGeneration() {
399
- if (_llmStoppingCriteria && _llmGenerating) {
400
- _llmStoppingCriteria.interrupt();
401
- console.log('[LLM Adapter] Generation cancelled by user');
402
- }
403
- }
404
-
405
- /**
406
- * Kiểm tra LLM đang generate hay không.
407
- * @returns {boolean}
408
- */
409
- function isLLMGenerating() {
410
- return _llmGenerating;
411
- }
412
-
413
- /**
414
- * Lấy lỗi cuối cùng từ LLM adapter (để hiển thị debug info).
415
- * @returns {string|null}
416
- */
417
- function getLLMLastError() {
418
- return _llmLastError;
419
- }
420
-
421
- /**
422
- * Kiểm tra LLM đã sẵn sàng chưa.
423
- * @returns {boolean}
424
- */
425
- function isLLMReady() {
426
- return _llmReady;
427
- }
428
-
429
- /**
430
- * Lấy trạng thái LLM.
431
- * @returns {{ready: boolean, loading: boolean, error: string|null, modelId: string}}
432
- */
433
- function getLLMStatus() {
434
- return {
435
- ready: _llmReady,
436
- loading: _llmLoading,
437
- error: _llmLoadError,
438
- modelId: LLM_MODEL_ID
439
- };
440
- }
441
-
442
- // Node/test: export to globalThis
443
- if (typeof module !== 'undefined' && module.exports) {
444
- globalThis.llmAdapter = llmAdapter;
445
- globalThis.loadLLMModel = loadLLMModel;
446
- globalThis.llmGenerate = llmGenerate;
447
- globalThis.llmGenerateWithImage = llmGenerateWithImage;
448
- globalThis.isWebGPUSupported = isWebGPUSupported;
449
- globalThis.isLLMReady = isLLMReady;
450
- globalThis.getLLMStatus = getLLMStatus;
451
- globalThis.getLLMLastError = getLLMLastError;
452
- globalThis.cancelLLMGeneration = cancelLLMGeneration;
453
- globalThis.isLLMGenerating = isLLMGenerating;
454
- globalThis.setLLMModelId = setLLMModelId;
455
- globalThis.setLLMThinkingEnabled = setLLMThinkingEnabled;
456
- globalThis.isLLMThinkingEnabled = isLLMThinkingEnabled;
457
- globalThis.setLLMStatusCallback = setLLMStatusCallback;
458
- globalThis.LLM_MODEL_ID = LLM_MODEL_ID;
459
- globalThis.LLM_MAX_NEW_TOKENS = LLM_MAX_NEW_TOKENS;
460
- }
 
 
1
+ // ============================================================
2
+ // LLM Adapter — Chạy LLM trực tiếp trên trình duyệt qua WebGPU
3
+ // Tham khảo: webml-community/Qwen3.5-WebGPU (HuggingFace Spaces)
4
+ // Sử dụng @huggingface/transformers để load model và generate text
5
+ // Phụ thuộc: currentLang, _adapterPath
6
+ // ============================================================
7
+
8
+ /**
9
+ * Trạng thái LLM — singleton, load model 1 lần duy nhất.
10
+ */
11
+ var _llmProcessor = null;
12
+ var _llmModel = null;
13
+ var _llmLoading = false;
14
+ var _llmReady = false;
15
+ var _llmLoadError = null;
16
+ var _llmTransformers = null; // Lưu ref module transformers để dùng RawImage
17
+ var _llmLastError = null; // Lỗi cuối cùng từ LLM (để hiển thị cho user debug)
18
+ var _llmStoppingCriteria = null; // InterruptableStoppingCriteria để cancel generate
19
+ var _llmGenerating = false; // Đang generate hay không
20
+ var _llmThinkingEnabled = false; // Mặc định tắt thinking
21
+
22
+ /**
23
+ * Callback để thông báo trạng thái loading lên UI.
24
+ * Được set từ app.js qua setLLMStatusCallback().
25
+ * @type {function(string, string)|null}
26
+ * - action: 'loading_start' | 'loading_progress' | 'loading_done' | 'loading_error'
27
+ * - message: Mô tả trạng thái
28
+ */
29
+ var _llmStatusCallback = null;
30
+
31
+ /**
32
+ * Đăng ký callback nhận thông báo trạng thái loading.
33
+ * @param {function(string, string)} callback - fn(action, message)
34
+ */
35
+ function setLLMStatusCallback(callback) {
36
+ _llmStatusCallback = typeof callback === 'function' ? callback : null;
37
+ }
38
+
39
+ function _notifyStatus(action, message) {
40
+ if (typeof _llmStatusCallback === 'function') {
41
+ try { _llmStatusCallback(action, message); } catch (e) { /* ignore */ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Model ID mặc định — Qwen3.5 0.6B (nhỏ nhất, phù hợp chạy trên browser).
47
+ * Có thể thay đổi bằng cách gọi setLLMModelId() trước khi load.
48
+ */
49
+ var LLM_MODEL_ID = 'onnx-community/Qwen3.5-0.8B-ONNX-OPT';
50
+
51
+ /**
52
+ * Max tokens cho mỗi lần generate.
53
+ */
54
+ var LLM_MAX_NEW_TOKENS = 256;
55
+
56
+ /**
57
+ * Thay đổi model ID (phải gọi trước loadLLMModel).
58
+ * @param {string} modelId - HuggingFace model ID
59
+ */
60
+ function setLLMModelId(modelId) {
61
+ if (typeof modelId === 'string' && modelId.trim()) {
62
+ LLM_MODEL_ID = modelId.trim();
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Bật/tắt thinking mode.
68
+ * @param {boolean} enabled
69
+ */
70
+ function setLLMThinkingEnabled(enabled) {
71
+ _llmThinkingEnabled = !!enabled;
72
+ try { localStorage.setItem('hikari_llm_thinking', _llmThinkingEnabled ? '1' : '0'); } catch (e) {}
73
+ }
74
+
75
+ /**
76
+ * Kiểm tra thinking mode có đang bật không.
77
+ * @returns {boolean}
78
+ */
79
+ function isLLMThinkingEnabled() {
80
+ return _llmThinkingEnabled;
81
+ }
82
+
83
+ /**
84
+ * Build prompt string từ history + message hiện tại.
85
+ * History được truyền từ app.js, đã trim sẵn.
86
+ * @param {string} userMessage
87
+ * @param {boolean} hasImage
88
+ * @param {Array<{role: string, content: string}>} [history] - Lịch sử hội thoại đã trim
89
+ * @returns {string}
90
+ */
91
+ function _buildPromptWithHistory(userMessage, hasImage, history) {
92
+ var prompt = '';
93
+
94
+ // Thêm history nếu
95
+ if (history && history.length > 0) {
96
+ for (var i = 0; i < history.length; i++) {
97
+ var msg = history[i];
98
+ prompt += '<|im_start|>' + msg.role + '\n' + msg.content + '<|im_end|>\n';
99
+ }
100
+ }
101
+
102
+ // Thêm message hiện tại
103
+ prompt += '<|im_start|>user\n';
104
+ if (hasImage) {
105
+ prompt += '<|vision_start|><|image_pad|><|vision_end|>';
106
+ }
107
+ prompt += userMessage + '<|im_end|>\n';
108
+
109
+ // Assistant prefix
110
+ prompt += '<|im_start|>assistant\n';
111
+ if (_llmThinkingEnabled) {
112
+ prompt += '<think>\n';
113
+ }
114
+
115
+ return prompt;
116
+ }
117
+
118
+ /**
119
+ * Kiểm tra trình duyệt có hỗ trợ WebGPU không.
120
+ * @returns {boolean}
121
+ */
122
+ function isWebGPUSupported() {
123
+ return typeof navigator !== 'undefined' && !!navigator.gpu;
124
+ }
125
+
126
+ /**
127
+ * Load model processor (lazy chỉ load khi cần lần đầu).
128
+ * Tham khảo cách Qwen3.5-WebGPU load model với dtype q4 và device webgpu.
129
+ * @returns {Promise<boolean>} true nếu load thành công
130
+ */
131
+ async function loadLLMModel() {
132
+ if (_llmReady) return true;
133
+ if (_llmLoadError) return false;
134
+ if (_llmLoading) {
135
+ // Đợi nếu đang load
136
+ return new Promise(function (resolve) {
137
+ var check = setInterval(function () {
138
+ if (!_llmLoading) {
139
+ clearInterval(check);
140
+ resolve(_llmReady);
141
+ }
142
+ }, 200);
143
+ });
144
+ }
145
+
146
+ _llmLoading = true;
147
+
148
+ try {
149
+ // Dynamic import — @huggingface/transformers từ CDN
150
+ var transformers;
151
+ if (typeof module !== 'undefined' && module.exports) {
152
+ // Node/test: skip WebGPU không khả dụng
153
+ _llmLoadError = 'WebGPU not available in Node.js';
154
+ _llmLoading = false;
155
+ return false;
156
+ }
157
+
158
+ // Browser: import từ CDN (giống Qwen3.5-WebGPU)
159
+ transformers = await import(
160
+ 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0'
161
+ );
162
+ _llmTransformers = transformers;
163
+
164
+ // Khởi tạo stopping criteria để hỗ trợ cancel
165
+ _llmStoppingCriteria = new transformers.InterruptableStoppingCriteria();
166
+
167
+ if (!isWebGPUSupported()) {
168
+ _llmLoadError = 'WebGPU not supported';
169
+ _llmLoading = false;
170
+ return false;
171
+ }
172
+
173
+ console.log('[LLM Adapter] Loading processor:', LLM_MODEL_ID);
174
+ _notifyStatus('loading_start', 'Loading processor...');
175
+ _llmProcessor = await transformers.AutoProcessor.from_pretrained(LLM_MODEL_ID);
176
+
177
+ console.log('[LLM Adapter] Loading model:', LLM_MODEL_ID);
178
+ _notifyStatus('loading_progress', 'Loading model weights...');
179
+ _llmModel = await transformers.Qwen3_5ForConditionalGeneration.from_pretrained(
180
+ LLM_MODEL_ID,
181
+ {
182
+ dtype: {
183
+ embed_tokens: 'q4',
184
+ vision_encoder: 'fp16',
185
+ decoder_model_merged: 'q4'
186
+ },
187
+ device: 'webgpu'
188
+ }
189
+ );
190
+
191
+ _llmReady = true;
192
+ _llmLoading = false;
193
+ console.log('[LLM Adapter] Model loaded successfully');
194
+ _notifyStatus('loading_done', 'Model loaded successfully');
195
+ return true;
196
+ } catch (err) {
197
+ console.error('[LLM Adapter] Failed to load model:', err);
198
+ _llmLoadError = err.message || 'Unknown error';
199
+ _llmLoading = false;
200
+ _notifyStatus('loading_error', 'Failed to load model: ' + _llmLoadError);
201
+ return false;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Helper: dispose past_key_values để giải phóng bộ nhớ GPU.
207
+ */
208
+ function _disposePastKeyValues(pastKeyValues) {
209
+ if (!pastKeyValues) return;
210
+ try {
211
+ for (var key of Object.keys(pastKeyValues)) {
212
+ if (pastKeyValues[key] && typeof pastKeyValues[key].dispose === 'function') {
213
+ pastKeyValues[key].dispose();
214
+ }
215
+ }
216
+ } catch (e) { /* ignore */ }
217
+ }
218
+
219
+ /**
220
+ * Generate text từ LLM cho một prompt.
221
+ * Sử dụng TextStreamer để thu thập response token-by-token.
222
+ * @param {string} userMessage - Tin nhắn người dùng
223
+ * @param {function} [onToken] - Callback nhận text tích lũy mỗi khi có token mới: fn(accumulatedText)
224
+ * @param {Array} [history] - Lịch sử hội thoại đã trim
225
+ * @returns {Promise<string|null>} Phản hồi từ LLM hoặc null nếu lỗi
226
+ */
227
+ async function llmGenerate(userMessage, onToken, history) {
228
+ if (!_llmReady || !_llmModel || !_llmProcessor || !_llmTransformers) return null;
229
+
230
+ _llmGenerating = true;
231
+ try {
232
+ var prompt = _buildPromptWithHistory(userMessage, false, history);
233
+
234
+ var inputs = await _llmProcessor(prompt);
235
+
236
+ var fullText = '';
237
+ var streamer = new _llmTransformers.TextStreamer(_llmProcessor.tokenizer, {
238
+ skip_prompt: true,
239
+ skip_special_tokens: true,
240
+ callback_function: function (token) {
241
+ fullText += token;
242
+ if (typeof onToken === 'function') {
243
+ onToken(fullText);
244
+ }
245
+ }
246
+ });
247
+
248
+ if (_llmStoppingCriteria) _llmStoppingCriteria.reset();
249
+
250
+ var generateOpts = {
251
+ ...inputs,
252
+ max_new_tokens: LLM_MAX_NEW_TOKENS,
253
+ do_sample: true,
254
+ streamer: streamer,
255
+ return_dict_in_generate: true
256
+ };
257
+ if (_llmStoppingCriteria) {
258
+ generateOpts.stopping_criteria = _llmStoppingCriteria;
259
+ }
260
+
261
+ var result = await _llmModel.generate(generateOpts);
262
+
263
+ _disposePastKeyValues(result.past_key_values);
264
+
265
+ var trimmed = fullText.replace(/^\n+/, '').trim();
266
+ _llmGenerating = false;
267
+ return trimmed.length > 0 ? trimmed : null;
268
+ } catch (err) {
269
+ console.error('[LLM Adapter] Generate error:', err);
270
+ _llmLastError = 'Generate error: ' + (err.message || err);
271
+ _llmGenerating = false;
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Generate text từ LLM cho một prompt kèm image.
278
+ * Sử dụng TextStreamer để thu thập response token-by-token.
279
+ * @param {string} userMessage - Tin nhắn người dùng
280
+ * @param {string} imageDataURL - Data URL hoặc blob URL của ảnh
281
+ * @param {function} [onToken] - Callback nhận text tích lũy mỗi khi có token mới: fn(accumulatedText)
282
+ * @param {Array} [history] - Lịch sử hội thoại đã trim
283
+ * @returns {Promise<string|null>} Phản hồi từ LLM hoặc null nếu lỗi
284
+ */
285
+ async function llmGenerateWithImage(userMessage, imageDataURL, onToken, history) {
286
+ if (!_llmReady || !_llmModel || !_llmProcessor || !_llmTransformers) return null;
287
+
288
+ _llmGenerating = true;
289
+ try {
290
+ var rawImage = await _llmTransformers.RawImage.read(imageDataURL);
291
+ var resizedImage = await rawImage.resize(448, 448);
292
+
293
+ var prompt = _buildPromptWithHistory(userMessage || '', true, history);
294
+
295
+ var inputs = await _llmProcessor(prompt, resizedImage);
296
+
297
+ var fullText = '';
298
+ var streamer = new _llmTransformers.TextStreamer(_llmProcessor.tokenizer, {
299
+ skip_prompt: true,
300
+ skip_special_tokens: true,
301
+ callback_function: function (token) {
302
+ fullText += token;
303
+ if (typeof onToken === 'function') {
304
+ onToken(fullText);
305
+ }
306
+ }
307
+ });
308
+
309
+ if (_llmStoppingCriteria) _llmStoppingCriteria.reset();
310
+
311
+ var generateOpts = {
312
+ ...inputs,
313
+ max_new_tokens: LLM_MAX_NEW_TOKENS,
314
+ do_sample: true,
315
+ streamer: streamer,
316
+ return_dict_in_generate: true
317
+ };
318
+ if (_llmStoppingCriteria) {
319
+ generateOpts.stopping_criteria = _llmStoppingCriteria;
320
+ }
321
+
322
+ var result = await _llmModel.generate(generateOpts);
323
+
324
+ _disposePastKeyValues(result.past_key_values);
325
+
326
+ var trimmed = fullText.replace(/^\n+/, '').trim();
327
+ _llmGenerating = false;
328
+
329
+ return trimmed.length > 0 ? trimmed : null;
330
+ } catch (err) {
331
+ console.error('[LLM Adapter] Generate with image error:', err);
332
+ _llmLastError = 'Generate with image error: ' + (err.message || err);
333
+ _llmGenerating = false;
334
+ return null;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * LLM Adapter Interface chính, tương thích với hệ thống adapter.
340
+ * thể dùng như subroutine hoặc gọi trực tiếp.
341
+ * @param {object} rs - RiveScript instance ( thể null)
342
+ * @param {string[]} args - Mảng từ (input đã split)
343
+ * @param {string} [imageDataURL] - Data URL của ảnh đính kèm (optional)
344
+ * @param {function} [onToken] - Callback stream token realtime: fn(accumulatedText)
345
+ * @param {Array} [history] - Lịch sử hội thoại đã trim từ app.js
346
+ * @returns {Promise<string|null>} Phản hồi từ LLM hoặc null
347
+ */
348
+ async function llmAdapter(rs, args, imageDataURL, onToken, history) {
349
+ _adapterPath.push('llm_adapter');
350
+ _llmLastError = null;
351
+ var query = (args || []).join(' ').trim();
352
+ var lang = currentLang || 'vi';
353
+
354
+ // Cho phép gửi ảnh không cần text (mô tả ảnh)
355
+ if (query.length === 0 && !imageDataURL) {
356
+ if (lang === 'en') return 'Please provide a message.';
357
+ if (lang === 'ja') return 'メッセージを入力してください。';
358
+ return 'Vui lòng nhập tin nhắn.';
359
+ }
360
+
361
+ // Nếu chỉ ảnh không có text, thêm prompt mặc định
362
+ if (query.length === 0 && imageDataURL) {
363
+ if (lang === 'en') query = 'Describe this image in detail.';
364
+ else if (lang === 'ja') query = 'この画像を詳しく説明してください。';
365
+ else query = 'Hãy mô tả chi tiết hình ảnh này.';
366
+ }
367
+
368
+ // Kiểm tra WebGPU
369
+ if (!isWebGPUSupported()) {
370
+ _llmLastError = 'WebGPU not supported in this browser';
371
+ return null;
372
+ }
373
+
374
+ // Load model nếu chưa load
375
+ var loaded = await loadLLMModel();
376
+ if (!loaded) {
377
+ _llmLastError = 'Model load failed: ' + (_llmLoadError || 'unknown');
378
+ return null;
379
+ }
380
+
381
+ // Generate không timeout, chờ LLM chạy hết
382
+ var generateFn = imageDataURL
383
+ ? llmGenerateWithImage(query, imageDataURL, onToken, history)
384
+ : llmGenerate(query, onToken, history);
385
+
386
+ var result = await generateFn;
387
+
388
+ if (result) return result;
389
+
390
+ if (!_llmLastError) {
391
+ _llmLastError = 'Generate returned empty response';
392
+ }
393
+ return null;
394
+ }
395
+
396
+ /**
397
+ * Hủy bỏ quá trình generate đang chạy.
398
+ */
399
+ function cancelLLMGeneration() {
400
+ if (_llmStoppingCriteria && _llmGenerating) {
401
+ _llmStoppingCriteria.interrupt();
402
+ console.log('[LLM Adapter] Generation cancelled by user');
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Kiểm tra LLM đang generate hay không.
408
+ * @returns {boolean}
409
+ */
410
+ function isLLMGenerating() {
411
+ return _llmGenerating;
412
+ }
413
+
414
+ /**
415
+ * Lấy lỗi cuối cùng từ LLM adapter (để hiển thị debug info).
416
+ * @returns {string|null}
417
+ */
418
+ function getLLMLastError() {
419
+ return _llmLastError;
420
+ }
421
+
422
+ /**
423
+ * Kiểm tra LLM đã sẵn sàng chưa.
424
+ * @returns {boolean}
425
+ */
426
+ function isLLMReady() {
427
+ return _llmReady;
428
+ }
429
+
430
+ /**
431
+ * Lấy trạng thái LLM.
432
+ * @returns {{ready: boolean, loading: boolean, error: string|null, modelId: string}}
433
+ */
434
+ function getLLMStatus() {
435
+ return {
436
+ ready: _llmReady,
437
+ loading: _llmLoading,
438
+ error: _llmLoadError,
439
+ modelId: LLM_MODEL_ID
440
+ };
441
+ }
442
+
443
+ // Node/test: export to globalThis
444
+ if (typeof module !== 'undefined' && module.exports) {
445
+ globalThis.llmAdapter = llmAdapter;
446
+ globalThis.loadLLMModel = loadLLMModel;
447
+ globalThis.llmGenerate = llmGenerate;
448
+ globalThis.llmGenerateWithImage = llmGenerateWithImage;
449
+ globalThis.isWebGPUSupported = isWebGPUSupported;
450
+ globalThis.isLLMReady = isLLMReady;
451
+ globalThis.getLLMStatus = getLLMStatus;
452
+ globalThis.getLLMLastError = getLLMLastError;
453
+ globalThis.cancelLLMGeneration = cancelLLMGeneration;
454
+ globalThis.isLLMGenerating = isLLMGenerating;
455
+ globalThis.setLLMModelId = setLLMModelId;
456
+ globalThis.setLLMThinkingEnabled = setLLMThinkingEnabled;
457
+ globalThis.isLLMThinkingEnabled = isLLMThinkingEnabled;
458
+ globalThis.setLLMStatusCallback = setLLMStatusCallback;
459
+ globalThis.LLM_MODEL_ID = LLM_MODEL_ID;
460
+ globalThis.LLM_MAX_NEW_TOKENS = LLM_MAX_NEW_TOKENS;
461
+ }
adapters/logic-dispatcher.js CHANGED
@@ -1,64 +1,71 @@
1
- // ============================================================
2
- // Logic Adapter Dispatcher — Điều phối các adapter con theo ưu tiên
3
- // Phụ thuộc: currentLang, _adapterPath, specificResponseAdapter, timeAdapter,
4
- // mathematicalEvaluationAdapter, unitConversionAdapter, bestMatchAdapter
5
- // ============================================================
6
-
7
- var INVALID_RESPONSE_PHRASES = [
8
- 'không có phản hồi cụ thể', 'không hiểu', 'không thể', 'không tìm thấy',
9
- 'không tìm được', 'cú pháp không hợp lệ',
10
- 'no specific response', "don't understand", 'cannot', 'not found',
11
- 'invalid syntax', 'could not find',
12
- '特定の応答はありません', '理解できません', 'できません', '見つかりません',
13
- '構文が無効', '見つかりませんでした'
14
- ];
15
-
16
- function isValidAdapterResult(result) {
17
- if (typeof result !== 'string' || result.trim().length === 0) return false;
18
- var lower = result.toLowerCase();
19
- for (var i = 0; i < INVALID_RESPONSE_PHRASES.length; i++) {
20
- if (lower.indexOf(INVALID_RESPONSE_PHRASES[i].toLowerCase()) !== -1) return false;
21
- }
22
- return true;
23
- }
24
-
25
- function logicAdapterDispatcher(rs, args) {
26
- _adapterPath.push('logic_adapter');
27
- var adapters = [
28
- specificResponseAdapter,
29
- timeAdapter,
30
- mathematicalEvaluationAdapter,
31
- unitConversionAdapter,
32
- bestMatchAdapter
33
- ];
34
-
35
- var pathLenBefore = _adapterPath.length;
36
-
37
- for (var i = 0; i < adapters.length; i++) {
38
- try {
39
- _adapterPath.length = pathLenBefore;
40
- var result = adapters[i](rs, args);
41
- // bestMatchAdapter trả {answer, score}, các adapter khác trả string
42
- if (result && typeof result === 'object' && result.answer) {
43
- if (isValidAdapterResult(result.answer)) return result.answer;
44
- } else if (isValidAdapterResult(result)) {
45
- return result;
46
- }
47
- } catch (err) {
48
- continue;
49
- }
50
- }
51
-
52
- _adapterPath.length = pathLenBefore;
53
- var lang = currentLang || 'vi';
54
- if (lang === 'en') return 'Sorry, I cannot process your request at this time.';
55
- if (lang === 'ja') return '申し訳ありませんが、現在リクエストを処理できません。';
56
- return 'Xin lỗi, mình không thể xử lý yêu cầu của bạn lúc này.';
57
- }
58
-
59
- // Node/test: export to globalThis
60
- if (typeof module !== 'undefined' && module.exports) {
61
- globalThis.INVALID_RESPONSE_PHRASES = INVALID_RESPONSE_PHRASES;
62
- globalThis.isValidAdapterResult = isValidAdapterResult;
63
- globalThis.logicAdapterDispatcher = logicAdapterDispatcher;
64
- }
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // Logic Adapter Dispatcher — Điều phối các adapter con theo ưu tiên
3
+ // Phụ thuộc: currentLang, _adapterPath, specificResponseAdapter, timeAdapter,
4
+ // mathematicalEvaluationAdapter, unitConversionAdapter, bestMatchAdapter
5
+ // ============================================================
6
+
7
+ var INVALID_RESPONSE_PHRASES = [
8
+ 'không có phản hồi cụ thể', 'không hiểu', 'không thể', 'không tìm thấy',
9
+ 'không tìm được', 'cú pháp không hợp lệ',
10
+ 'no specific response', "don't understand", 'cannot', 'not found',
11
+ 'invalid syntax', 'could not find',
12
+ '特定の応答はありません', '理解できません', 'できません', '見つかりません',
13
+ '構文が無効', '見つかりませんでした'
14
+ ];
15
+
16
+ function isValidAdapterResult(result) {
17
+ if (typeof result !== 'string' || result.trim().length === 0) return false;
18
+ var lower = result.toLowerCase();
19
+ for (var i = 0; i < INVALID_RESPONSE_PHRASES.length; i++) {
20
+ if (lower.indexOf(INVALID_RESPONSE_PHRASES[i].toLowerCase()) !== -1) return false;
21
+ }
22
+ return true;
23
+ }
24
+
25
+ function logicAdapterDispatcher(rs, args) {
26
+ _adapterPath.push('logic_adapter');
27
+ var adapterMap = [
28
+ { key: 'specific_response', fn: specificResponseAdapter },
29
+ { key: 'time_adapter', fn: timeAdapter },
30
+ { key: 'mathematical_evaluation', fn: mathematicalEvaluationAdapter },
31
+ { key: 'unit_conversion', fn: unitConversionAdapter },
32
+ { key: 'best_match', fn: bestMatchAdapter }
33
+ ];
34
+
35
+ var pathLenBefore = _adapterPath.length;
36
+
37
+ for (var i = 0; i < adapterMap.length; i++) {
38
+ var entry = adapterMap[i];
39
+ // Bỏ qua adapter bị disabled
40
+ if (typeof ADAPTER_REGISTRY !== 'undefined' &&
41
+ ADAPTER_REGISTRY[entry.key] &&
42
+ ADAPTER_REGISTRY[entry.key].active === false) {
43
+ continue;
44
+ }
45
+ try {
46
+ _adapterPath.length = pathLenBefore;
47
+ var result = entry.fn(rs, args);
48
+ // bestMatchAdapter trả {answer, score}, các adapter khác trả string
49
+ if (result && typeof result === 'object' && result.answer) {
50
+ if (isValidAdapterResult(result.answer)) return result.answer;
51
+ } else if (isValidAdapterResult(result)) {
52
+ return result;
53
+ }
54
+ } catch (err) {
55
+ continue;
56
+ }
57
+ }
58
+
59
+ _adapterPath.length = pathLenBefore;
60
+ var lang = currentLang || 'vi';
61
+ if (lang === 'en') return 'Sorry, I cannot process your request at this time.';
62
+ if (lang === 'ja') return '申し訳ありませんが、現在リクエストを処理できません。';
63
+ return 'Xin lỗi, mình không thể xử lý yêu cầu của bạn lúc này.';
64
+ }
65
+
66
+ // Node/test: export to globalThis
67
+ if (typeof module !== 'undefined' && module.exports) {
68
+ globalThis.INVALID_RESPONSE_PHRASES = INVALID_RESPONSE_PHRASES;
69
+ globalThis.isValidAdapterResult = isValidAdapterResult;
70
+ globalThis.logicAdapterDispatcher = logicAdapterDispatcher;
71
+ }
adapters/math-adapter.js CHANGED
@@ -1,77 +1,77 @@
1
- // ============================================================
2
- // Mathematical Evaluation Adapter — Tính toán biểu thức toán học
3
- // Phụ thuộc: currentLang, _adapterPath
4
- // ============================================================
5
-
6
- /**
7
- * Trích xuất và tính toán biểu thức toán học từ chuỗi đầu vào.
8
- * Không sử dụng eval() — dùng regex + switch/case.
9
- */
10
- function parseMathExpression(input, lang) {
11
- if (typeof input !== 'string' || input.trim().length === 0) {
12
- return { error: lang === 'en' ? 'Invalid expression.' : lang === 'ja' ? '無効な式です。' : 'Biểu thức không hợp lệ.' };
13
- }
14
-
15
- var expr = input.trim();
16
- if (lang !== 'ja') expr = expr.toLowerCase();
17
-
18
- if (lang === 'vi') {
19
- expr = expr.replace(/cộng/g, '+').replace(/trừ/g, '-').replace(/nhân/g, '*').replace(/chia/g, '/');
20
- } else if (lang === 'en') {
21
- expr = expr.replace(/divided\s+by/g, '/').replace(/plus/g, '+').replace(/minus/g, '-').replace(/times/g, '*');
22
- } else if (lang === 'ja') {
23
- expr = expr.replace(/足す/g, '+').replace(/引く/g, '-').replace(/掛ける/g, '*').replace(/割る/g, '/');
24
- }
25
-
26
- expr = expr.replace(/×/g, '*').replace(/÷/g, '/');
27
-
28
- var match = expr.match(/(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/);
29
- if (!match) {
30
- return { error: lang === 'en' ? 'Cannot parse the mathematical expression.' : lang === 'ja' ? '数式を解析できませんでした。' : 'Không thể phân tích biểu thức toán học.' };
31
- }
32
-
33
- var a = parseFloat(match[1]);
34
- var operator = match[2];
35
- var b = parseFloat(match[3]);
36
- var result;
37
-
38
- switch (operator) {
39
- case '+': result = a + b; break;
40
- case '-': result = a - b; break;
41
- case '*': result = a * b; break;
42
- case '/':
43
- if (b === 0) return { error: lang === 'en' ? 'Cannot divide by zero.' : lang === 'ja' ? 'ゼロで割ることはできません。' : 'Không thể chia cho 0.' };
44
- result = a / b; break;
45
- default:
46
- return { error: lang === 'en' ? 'Unsupported operator.' : lang === 'ja' ? 'サポートされていない演算子です。' : 'Phép tính không được hỗ trợ.' };
47
- }
48
-
49
- return { result: result };
50
- }
51
-
52
- function mathematicalEvaluationAdapter(rs, args) {
53
- _adapterPath.push('mathematical_evaluation');
54
- var input = (args || []).join(' ').trim();
55
- var lang = currentLang || 'vi';
56
-
57
- if (input.length === 0) {
58
- return lang === 'en' ? 'Please provide a mathematical expression.'
59
- : lang === 'ja' ? '数式を入力してください。'
60
- : 'Vui lòng nhập biểu thức toán học.';
61
- }
62
-
63
- var parsed = parseMathExpression(input, lang);
64
- if (parsed.error) return parsed.error;
65
-
66
- var resultStr = Number.isInteger(parsed.result) ? String(parsed.result) : parsed.result.toFixed(2).replace(/\.?0+$/, '');
67
-
68
- if (lang === 'en') return 'Result: ' + resultStr;
69
- if (lang === 'ja') return '結果: ' + resultStr;
70
- return 'Kết quả: ' + resultStr;
71
- }
72
-
73
- // Node/test: export to globalThis
74
- if (typeof module !== 'undefined' && module.exports) {
75
- globalThis.parseMathExpression = parseMathExpression;
76
- globalThis.mathematicalEvaluationAdapter = mathematicalEvaluationAdapter;
77
- }
 
1
+ // ============================================================
2
+ // Mathematical Evaluation Adapter — Tính toán biểu thức toán học
3
+ // Phụ thuộc: currentLang, _adapterPath
4
+ // ============================================================
5
+
6
+ /**
7
+ * Trích xuất và tính toán biểu thức toán học từ chuỗi đầu vào.
8
+ * Không sử dụng eval() — dùng regex + switch/case.
9
+ */
10
+ function parseMathExpression(input, lang) {
11
+ if (typeof input !== 'string' || input.trim().length === 0) {
12
+ return { error: lang === 'en' ? 'Invalid expression.' : lang === 'ja' ? '無効な式です。' : 'Biểu thức không hợp lệ.' };
13
+ }
14
+
15
+ var expr = input.trim();
16
+ if (lang !== 'ja') expr = expr.toLowerCase();
17
+
18
+ if (lang === 'vi') {
19
+ expr = expr.replace(/cộng/g, '+').replace(/trừ/g, '-').replace(/nhân/g, '*').replace(/chia/g, '/');
20
+ } else if (lang === 'en') {
21
+ expr = expr.replace(/divided\s+by/g, '/').replace(/plus/g, '+').replace(/minus/g, '-').replace(/times/g, '*');
22
+ } else if (lang === 'ja') {
23
+ expr = expr.replace(/足す/g, '+').replace(/引く/g, '-').replace(/掛ける/g, '*').replace(/割る/g, '/');
24
+ }
25
+
26
+ expr = expr.replace(/×/g, '*').replace(/÷/g, '/');
27
+
28
+ var match = expr.match(/(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/);
29
+ if (!match) {
30
+ return { error: lang === 'en' ? 'Cannot parse the mathematical expression.' : lang === 'ja' ? '数式を解析できませんでした。' : 'Không thể phân tích biểu thức toán học.' };
31
+ }
32
+
33
+ var a = parseFloat(match[1]);
34
+ var operator = match[2];
35
+ var b = parseFloat(match[3]);
36
+ var result;
37
+
38
+ switch (operator) {
39
+ case '+': result = a + b; break;
40
+ case '-': result = a - b; break;
41
+ case '*': result = a * b; break;
42
+ case '/':
43
+ if (b === 0) return { error: lang === 'en' ? 'Cannot divide by zero.' : lang === 'ja' ? 'ゼロで割ることはできません。' : 'Không thể chia cho 0.' };
44
+ result = a / b; break;
45
+ default:
46
+ return { error: lang === 'en' ? 'Unsupported operator.' : lang === 'ja' ? 'サポートされていない演算子です。' : 'Phép tính không được hỗ trợ.' };
47
+ }
48
+
49
+ return { result: result };
50
+ }
51
+
52
+ function mathematicalEvaluationAdapter(rs, args) {
53
+ _adapterPath.push('mathematical_evaluation');
54
+ var input = (args || []).join(' ').trim();
55
+ var lang = currentLang || 'vi';
56
+
57
+ if (input.length === 0) {
58
+ return lang === 'en' ? 'Please provide a mathematical expression.'
59
+ : lang === 'ja' ? '数式を入力してください。'
60
+ : 'Vui lòng nhập biểu thức toán học.';
61
+ }
62
+
63
+ var parsed = parseMathExpression(input, lang);
64
+ if (parsed.error) return parsed.error;
65
+
66
+ var resultStr = Number.isInteger(parsed.result) ? String(parsed.result) : parsed.result.toFixed(2).replace(/\.?0+$/, '');
67
+
68
+ if (lang === 'en') return 'Result: ' + resultStr;
69
+ if (lang === 'ja') return '結果: ' + resultStr;
70
+ return 'Kết quả: ' + resultStr;
71
+ }
72
+
73
+ // Node/test: export to globalThis
74
+ if (typeof module !== 'undefined' && module.exports) {
75
+ globalThis.parseMathExpression = parseMathExpression;
76
+ globalThis.mathematicalEvaluationAdapter = mathematicalEvaluationAdapter;
77
+ }
adapters/specific-response.js CHANGED
@@ -1,53 +1,53 @@
1
- // ============================================================
2
- // Specific Response Adapter — Phản hồi exact match từ bảng ánh xạ
3
- // Phụ thuộc: currentLang, _adapterPath, removeVietnameseDiacritics, SPECIFIC_RESPONSES
4
- // ============================================================
5
-
6
- /**
7
- * Specific Response Adapter — Object Macro trả về phản hồi exact match.
8
- * So khớp case-insensitive, hỗ trợ diacritics-stripped match cho tiếng Việt.
9
- *
10
- * @param {object} rs - RiveScript instance
11
- * @param {string[]} args - Mảng các từ từ thẻ <call>
12
- * @returns {string} Câu trả lời tương ứng hoặc thông báo "không có phản hồi cụ thể"
13
- */
14
- function specificResponseAdapter(rs, args) {
15
- _adapterPath.push('specific_response');
16
- var input = (args || []).join(' ').trim().toLowerCase();
17
-
18
- if (input.length === 0) {
19
- return currentLang === 'en'
20
- ? 'No specific response for this question.'
21
- : currentLang === 'ja'
22
- ? 'この質問に対する特定の応答はありません。'
23
- : 'Không có phản hồi cụ thể cho câu hỏi này.';
24
- }
25
-
26
- var responses = SPECIFIC_RESPONSES[currentLang] || SPECIFIC_RESPONSES['vi'];
27
-
28
- // Thử exact match trước
29
- var keys = Object.keys(responses);
30
- for (var i = 0; i < keys.length; i++) {
31
- if (keys[i].toLowerCase() === input) {
32
- return responses[keys[i]];
33
- }
34
- }
35
- // Thử match sau khi bỏ dấu (chỉ tiếng Việt)
36
- if (currentLang === 'vi') {
37
- var inputNoDiacritics = removeVietnameseDiacritics(input);
38
- for (var j = 0; j < keys.length; j++) {
39
- if (removeVietnameseDiacritics(keys[j].toLowerCase()) === inputNoDiacritics) {
40
- return responses[keys[j]];
41
- }
42
- }
43
- }
44
-
45
- if (currentLang === 'en') return 'No specific response for this question.';
46
- if (currentLang === 'ja') return 'この質問に対する特定の応答はありません。';
47
- return 'Không có phản hồi cụ thể cho câu hỏi này.';
48
- }
49
-
50
- // Node/test: export to globalThis
51
- if (typeof module !== 'undefined' && module.exports) {
52
- globalThis.specificResponseAdapter = specificResponseAdapter;
53
- }
 
1
+ // ============================================================
2
+ // Specific Response Adapter — Phản hồi exact match từ bảng ánh xạ
3
+ // Phụ thuộc: currentLang, _adapterPath, removeVietnameseDiacritics, SPECIFIC_RESPONSES
4
+ // ============================================================
5
+
6
+ /**
7
+ * Specific Response Adapter — Object Macro trả về phản hồi exact match.
8
+ * So khớp case-insensitive, hỗ trợ diacritics-stripped match cho tiếng Việt.
9
+ *
10
+ * @param {object} rs - RiveScript instance
11
+ * @param {string[]} args - Mảng các từ từ thẻ <call>
12
+ * @returns {string} Câu trả lời tương ứng hoặc thông báo "không có phản hồi cụ thể"
13
+ */
14
+ function specificResponseAdapter(rs, args) {
15
+ _adapterPath.push('specific_response');
16
+ var input = (args || []).join(' ').trim().toLowerCase();
17
+
18
+ if (input.length === 0) {
19
+ return currentLang === 'en'
20
+ ? 'No specific response for this question.'
21
+ : currentLang === 'ja'
22
+ ? 'この質問に対する特定の応答はありません。'
23
+ : 'Không có phản hồi cụ thể cho câu hỏi này.';
24
+ }
25
+
26
+ var responses = SPECIFIC_RESPONSES[currentLang] || SPECIFIC_RESPONSES['vi'];
27
+
28
+ // Thử exact match trước
29
+ var keys = Object.keys(responses);
30
+ for (var i = 0; i < keys.length; i++) {
31
+ if (keys[i].toLowerCase() === input) {
32
+ return responses[keys[i]];
33
+ }
34
+ }
35
+ // Thử match sau khi bỏ dấu (chỉ tiếng Việt)
36
+ if (currentLang === 'vi') {
37
+ var inputNoDiacritics = removeVietnameseDiacritics(input);
38
+ for (var j = 0; j < keys.length; j++) {
39
+ if (removeVietnameseDiacritics(keys[j].toLowerCase()) === inputNoDiacritics) {
40
+ return responses[keys[j]];
41
+ }
42
+ }
43
+ }
44
+
45
+ if (currentLang === 'en') return 'No specific response for this question.';
46
+ if (currentLang === 'ja') return 'この質問に対する特定の応答はありません。';
47
+ return 'Không có phản hồi cụ thể cho câu hỏi này.';
48
+ }
49
+
50
+ // Node/test: export to globalThis
51
+ if (typeof module !== 'undefined' && module.exports) {
52
+ globalThis.specificResponseAdapter = specificResponseAdapter;
53
+ }
adapters/text-similarity.js CHANGED
@@ -1,427 +1,427 @@
1
- // ============================================================
2
- // Text Similarity — Thuật toán so khớp chuỗi
3
- // Gồm 4 thuật toán: Levenshtein, Jaccard, Cosine, Synset
4
- // + Preprocessed data support (TF-IDF, pre-tokenized)
5
- // ============================================================
6
-
7
- // --- Preprocessed data cache ---
8
- // Loaded from data/preprocessed.json (build-time) hoặc localStorage (client)
9
- var _preprocessedData = null;
10
- var _PREPROCESSED_STORAGE_KEY = 'hikari_preprocessed';
11
-
12
- /**
13
- * Load preprocessed data.
14
- * - Browser: thử localStorage trước, nếu không có hoặc outdated thì fetch từ server
15
- * - Node/test: load trực tiếp từ file
16
- * @returns {Promise<void>}
17
- */
18
- async function loadPreprocessedData() {
19
- // Node/test environment
20
- if (typeof module !== 'undefined' && module.exports) {
21
- try {
22
- var fs = require('fs');
23
- var path = require('path');
24
- var filePath = path.join(__dirname, '..', 'data', 'preprocessed.json');
25
- _preprocessedData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
26
- } catch (e) {
27
- _preprocessedData = null;
28
- }
29
- return;
30
- }
31
-
32
- // Browser: thử localStorage
33
- try {
34
- var cached = localStorage.getItem(_PREPROCESSED_STORAGE_KEY);
35
- if (cached) {
36
- var parsed = JSON.parse(cached);
37
- // Fetch version từ server để check outdated
38
- var resp = await fetch('data/preprocessed.json', { method: 'HEAD' });
39
- // Dùng Last-Modified hoặc so sánh version
40
- if (parsed && parsed.version) {
41
- _preprocessedData = parsed;
42
- // Background check: fetch full file và so sánh version
43
- fetch('data/preprocessed.json').then(function(r) { return r.json(); }).then(function(fresh) {
44
- if (fresh.version !== parsed.version) {
45
- _preprocessedData = fresh;
46
- localStorage.setItem(_PREPROCESSED_STORAGE_KEY, JSON.stringify(fresh));
47
- }
48
- }).catch(function() {});
49
- return;
50
- }
51
- }
52
- } catch (e) { /* localStorage not available */ }
53
-
54
- // Browser: fetch từ server
55
- try {
56
- var response = await fetch('data/preprocessed.json');
57
- if (response.ok) {
58
- _preprocessedData = await response.json();
59
- try {
60
- localStorage.setItem(_PREPROCESSED_STORAGE_KEY, JSON.stringify(_preprocessedData));
61
- } catch (e) { /* quota exceeded */ }
62
- }
63
- } catch (e) {
64
- _preprocessedData = null;
65
- }
66
- }
67
-
68
- /**
69
- * Lấy preprocessed data cho ngôn ngữ hiện tại.
70
- * @param {string} lang
71
- * @returns {object|null} { idf, statements } hoặc null
72
- */
73
- function getPreprocessedLang(lang) {
74
- if (!_preprocessedData || !_preprocessedData.langs) return null;
75
- return _preprocessedData.langs[lang] || null;
76
- }
77
-
78
- // ---- Tokenize (dùng cho input mới) ----
79
-
80
- /**
81
- * Tokenize input text: lowercase, bỏ dấu (vi), bỏ punctuation, tách từ.
82
- * Dùng cùng logic với scripts/preprocess.js để đảm bảo consistency.
83
- * @param {string} text
84
- * @param {string} lang
85
- * @returns {string[]}
86
- */
87
- function tokenizeForSimilarity(text, lang) {
88
- var s = String(text).toLowerCase();
89
- if (lang === 'vi' && typeof removeVietnameseDiacritics === 'function') {
90
- s = removeVietnameseDiacritics(s);
91
- }
92
- s = s.replace(/[?!.,;:"""''`~()[\]{}\\|@#$%^&]/g, ' ');
93
- return s.split(/\s+/).filter(function(w) { return w.length > 0; });
94
- }
95
-
96
- // ---- 1. Levenshtein Distance ----
97
-
98
- function levenshteinDistance(a, b) {
99
- var strA = String(a);
100
- var strB = String(b);
101
- var lenA = strA.length;
102
- var lenB = strB.length;
103
-
104
- if (lenA === 0) return lenB;
105
- if (lenB === 0) return lenA;
106
-
107
- var prev = [];
108
- var curr = [];
109
- var i, j;
110
-
111
- for (j = 0; j <= lenB; j++) prev[j] = j;
112
-
113
- for (i = 1; i <= lenA; i++) {
114
- curr[0] = i;
115
- for (j = 1; j <= lenB; j++) {
116
- if (strA[i - 1] === strB[j - 1]) {
117
- curr[j] = prev[j - 1];
118
- } else {
119
- curr[j] = 1 + Math.min(prev[j - 1], prev[j], curr[j - 1]);
120
- }
121
- }
122
- var tmp = prev;
123
- prev = curr;
124
- curr = tmp;
125
- }
126
- return prev[lenB];
127
- }
128
-
129
- // ---- 2. Jaccard Similarity ----
130
-
131
- function jaccardSimilarity(a, b) {
132
- var wordsA = String(a).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
133
- var wordsB = String(b).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
134
-
135
- if (wordsA.length === 0 && wordsB.length === 0) return 0;
136
-
137
- var setA = {}, setB = {}, i;
138
- for (i = 0; i < wordsA.length; i++) setA[wordsA[i]] = true;
139
- for (i = 0; i < wordsB.length; i++) setB[wordsB[i]] = true;
140
-
141
- var intersection = 0, union = {};
142
- for (var k1 in setA) { union[k1] = true; if (setB[k1]) intersection++; }
143
- for (var k2 in setB) { union[k2] = true; }
144
-
145
- var unionSize = Object.keys(union).length;
146
- return unionSize === 0 ? 0 : intersection / unionSize;
147
- }
148
-
149
- // ---- 3. Cosine Similarity ----
150
-
151
- function cosineSimilarity(a, b) {
152
- var wordsA = String(a).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
153
- var wordsB = String(b).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
154
-
155
- if (wordsA.length === 0 || wordsB.length === 0) return 0;
156
-
157
- var tfA = {}, tfB = {}, i;
158
- for (i = 0; i < wordsA.length; i++) tfA[wordsA[i]] = (tfA[wordsA[i]] || 0) + 1;
159
- for (i = 0; i < wordsB.length; i++) tfB[wordsB[i]] = (tfB[wordsB[i]] || 0) + 1;
160
-
161
- var vocab = {};
162
- for (var ka in tfA) vocab[ka] = true;
163
- for (var kb in tfB) vocab[kb] = true;
164
-
165
- var dot = 0, magA = 0, magB = 0;
166
- var keys = Object.keys(vocab);
167
- for (i = 0; i < keys.length; i++) {
168
- var va = tfA[keys[i]] || 0;
169
- var vb = tfB[keys[i]] || 0;
170
- dot += va * vb;
171
- magA += va * va;
172
- magB += vb * vb;
173
- }
174
-
175
- magA = Math.sqrt(magA);
176
- magB = Math.sqrt(magB);
177
- return (magA === 0 || magB === 0) ? 0 : dot / (magA * magB);
178
- }
179
-
180
- /**
181
- * Cosine similarity sử dụng TF-IDF vectors (preprocessed).
182
- * Input vector được tính on-the-fly, corpus vector đã preprocessed.
183
- * @param {object} inputTFIDF - TF-IDF vector của input
184
- * @param {number} inputMag - Magnitude của input vector
185
- * @param {object} corpusTFIDF - TF-IDF vector đã preprocessed
186
- * @param {number} corpusMag - Magnitude đã preprocessed
187
- * @returns {number} [0, 1]
188
- */
189
- function cosineSimilarityTFIDF(inputTFIDF, inputMag, corpusTFIDF, corpusMag) {
190
- if (inputMag === 0 || corpusMag === 0) return 0;
191
-
192
- var dot = 0;
193
- // Iterate over smaller vector for efficiency
194
- for (var word in inputTFIDF) {
195
- if (corpusTFIDF[word]) {
196
- dot += inputTFIDF[word] * corpusTFIDF[word];
197
- }
198
- }
199
- return dot / (inputMag * corpusMag);
200
- }
201
-
202
- // ---- 4. Synset Similarity ----
203
-
204
- var SYNONYM_GROUPS = [
205
- ['xin chao','chao','hi','hello','hey','yo'],
206
- ['tam biet','bye','goodbye','hen gap lai','tot lanh'],
207
- ['cam on','thanks','thank','thank you'],
208
- ['ten','name','ai','who','la ai'],
209
- ['lam gi','lam duoc','co the','giup','help','what can'],
210
- ['may gio','gio','time','clock','bao gio'],
211
- ['ngay','hom nay','date','today','ngay may'],
212
- ['thu','thu may','day','what day'],
213
- ['tinh','calculate','math','cong','tru','nhan','chia','plus','minus'],
214
- ['doi','convert','sang','to','chuyen doi'],
215
- ['chatbot','bot','ai','robot','may','machine'],
216
- ['la gi','what is','what','gi','mean'],
217
- ['khoe','vui','happy','fine','good','ok'],
218
- ['tuoi','age','old','bao nhieu tuoi'],
219
- ['o dau','where','dau','location'],
220
- ['thich','like','love','yeu'],
221
- ['huong dan','cach','how','guide','help','su dung','use']
222
- ];
223
-
224
- var _synonymLookup = null;
225
- function _getSynonymLookup() {
226
- if (_synonymLookup) return _synonymLookup;
227
- _synonymLookup = {};
228
- for (var g = 0; g < SYNONYM_GROUPS.length; g++) {
229
- for (var w = 0; w < SYNONYM_GROUPS[g].length; w++) {
230
- var word = SYNONYM_GROUPS[g][w].toLowerCase();
231
- if (!_synonymLookup[word]) _synonymLookup[word] = [];
232
- _synonymLookup[word].push(g);
233
- }
234
- }
235
- return _synonymLookup;
236
- }
237
-
238
- function areSynonyms(wordA, wordB) {
239
- if (wordA === wordB) return true;
240
- var lookup = _getSynonymLookup();
241
- var groupsA = lookup[wordA];
242
- var groupsB = lookup[wordB];
243
- if (!groupsA || !groupsB) return false;
244
- for (var i = 0; i < groupsA.length; i++) {
245
- for (var j = 0; j < groupsB.length; j++) {
246
- if (groupsA[i] === groupsB[j]) return true;
247
- }
248
- }
249
- return false;
250
- }
251
-
252
- function synsetSimilarity(a, b) {
253
- var wordsA = String(a).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
254
- var wordsB = String(b).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
255
-
256
- if (wordsA.length === 0 || wordsB.length === 0) return 0;
257
-
258
- var matchAtoB = 0;
259
- for (var i = 0; i < wordsA.length; i++) {
260
- for (var j = 0; j < wordsB.length; j++) {
261
- if (areSynonyms(wordsA[i], wordsB[j])) { matchAtoB++; break; }
262
- }
263
- }
264
- var matchBtoA = 0;
265
- for (var m = 0; m < wordsB.length; m++) {
266
- for (var n = 0; n < wordsA.length; n++) {
267
- if (areSynonyms(wordsB[m], wordsA[n])) { matchBtoA++; break; }
268
- }
269
- }
270
-
271
- return ((matchAtoB / wordsA.length) + (matchBtoA / wordsB.length)) / 2;
272
- }
273
-
274
- /**
275
- * Synset similarity sử dụng preprocessed synonym group indices.
276
- * So sánh overlap giữa synGroups của input và corpus statement.
277
- * @param {number[]} inputGroups - Synonym group indices của input
278
- * @param {number[]} corpusGroups - Synonym group indices đã preprocessed
279
- * @returns {number} [0, 1]
280
- */
281
- function synsetSimilarityPreprocessed(inputGroups, corpusGroups) {
282
- if (inputGroups.length === 0 && corpusGroups.length === 0) return 0;
283
- if (inputGroups.length === 0 || corpusGroups.length === 0) return 0;
284
-
285
- var setB = {};
286
- for (var j = 0; j < corpusGroups.length; j++) setB[corpusGroups[j]] = true;
287
-
288
- var overlap = 0;
289
- for (var i = 0; i < inputGroups.length; i++) {
290
- if (setB[inputGroups[i]]) overlap++;
291
- }
292
-
293
- var total = {};
294
- for (var a = 0; a < inputGroups.length; a++) total[inputGroups[a]] = true;
295
- for (var b = 0; b < corpusGroups.length; b++) total[corpusGroups[b]] = true;
296
- var unionSize = Object.keys(total).length;
297
-
298
- return unionSize === 0 ? 0 : overlap / unionSize;
299
- }
300
-
301
- // ---- Combined: textSimilarity (general purpose) ----
302
-
303
- function textSimilarity(a, b) {
304
- var strA = String(a);
305
- var strB = String(b);
306
-
307
- if (strA === strB && strA.length > 0) return 1.0;
308
- if (strA.length === 0 && strB.length === 0) return 0;
309
-
310
- var maxLen = Math.max(strA.length, strB.length);
311
- var levSim = maxLen > 0 ? 1 - (levenshteinDistance(strA, strB) / maxLen) : 0;
312
- var jacSim = jaccardSimilarity(strA, strB);
313
- var cosSim = cosineSimilarity(strA, strB);
314
- var synSim = synsetSimilarity(strA, strB);
315
-
316
- return Math.max(0, Math.min(1, (levSim + jacSim + cosSim + synSim) / 4));
317
- }
318
-
319
- // ---- Optimized: findBestMatchPreprocessed ----
320
-
321
- /**
322
- * Tìm câu trả lời phù hợp nhất từ preprocessed data.
323
- * Sử dụng TF-IDF cosine + synset preprocessed + Jaccard + Levenshtein.
324
- * Nhanh hơn textSimilarity() vì corpus đã được tiền xử lý.
325
- *
326
- * @param {string} input - Input người dùng (raw)
327
- * @param {string} lang - Ngôn ngữ hiện tại
328
- * @param {number} threshold - Ngưỡng similarity (mặc định 0.3)
329
- * @returns {{answer: string, score: number}|null} Kết quả hoặc null
330
- */
331
- function findBestMatchPreprocessed(input, lang, threshold) {
332
- threshold = threshold || 0.3;
333
- var ppLang = getPreprocessedLang(lang);
334
- if (!ppLang) return null; // Fallback: caller sẽ dùng textSimilarity thường
335
-
336
- var idf = ppLang.idf;
337
- var statements = ppLang.statements;
338
-
339
- // Tokenize input
340
- var inputTokens = tokenizeForSimilarity(input, lang);
341
- if (inputTokens.length === 0) return null;
342
-
343
- // Build input TF
344
- var inputTF = {};
345
- for (var t = 0; t < inputTokens.length; t++) {
346
- inputTF[inputTokens[t]] = (inputTF[inputTokens[t]] || 0) + 1;
347
- }
348
-
349
- // Build input TF-IDF
350
- var inputTFIDF = {};
351
- for (var word in inputTF) {
352
- inputTFIDF[word] = inputTF[word] * (idf[word] || 1);
353
- }
354
- var inputMag = 0;
355
- for (var w in inputTFIDF) inputMag += inputTFIDF[w] * inputTFIDF[w];
356
- inputMag = Math.sqrt(inputMag);
357
-
358
- // Build input synonym groups
359
- var lookup = _getSynonymLookup();
360
- var inputSynGroups = [];
361
- var seenGroups = {};
362
- for (var s = 0; s < inputTokens.length; s++) {
363
- var groups = lookup[inputTokens[s]];
364
- if (groups) {
365
- for (var gi = 0; gi < groups.length; gi++) {
366
- if (!seenGroups[groups[gi]]) {
367
- inputSynGroups.push(groups[gi]);
368
- seenGroups[groups[gi]] = true;
369
- }
370
- }
371
- }
372
- }
373
-
374
- // Input normalized string (for Levenshtein/Jaccard)
375
- var inputNorm = inputTokens.join(' ');
376
-
377
- // Compare with all preprocessed statements
378
- var bestScore = -1;
379
- var bestAnswer = null;
380
-
381
- for (var i = 0; i < statements.length; i++) {
382
- var stmt = statements[i];
383
- var stmtNorm = stmt.tokens.join(' ');
384
-
385
- // 1. Levenshtein similarity
386
- var maxLen = Math.max(inputNorm.length, stmtNorm.length);
387
- var levSim = maxLen > 0 ? 1 - (levenshteinDistance(inputNorm, stmtNorm) / maxLen) : 0;
388
-
389
- // 2. Jaccard (on tokens)
390
- var jacSim = jaccardSimilarity(inputNorm, stmtNorm);
391
-
392
- // 3. TF-IDF Cosine (preprocessed)
393
- var cosSim = cosineSimilarityTFIDF(inputTFIDF, inputMag, stmt.tfidf, stmt.magnitude);
394
-
395
- // 4. Synset (preprocessed)
396
- var synSim = synsetSimilarityPreprocessed(inputSynGroups, stmt.synGroups);
397
-
398
- var score = (levSim + jacSim + cosSim + synSim) / 4;
399
-
400
- if (score > bestScore) {
401
- bestScore = score;
402
- bestAnswer = stmt.answer;
403
- }
404
- }
405
-
406
- if (bestScore >= threshold) {
407
- return { answer: bestAnswer, score: bestScore };
408
- }
409
- return null;
410
- }
411
-
412
- // Node/test: export to globalThis
413
- if (typeof module !== 'undefined' && module.exports) {
414
- globalThis.levenshteinDistance = levenshteinDistance;
415
- globalThis.jaccardSimilarity = jaccardSimilarity;
416
- globalThis.cosineSimilarity = cosineSimilarity;
417
- globalThis.cosineSimilarityTFIDF = cosineSimilarityTFIDF;
418
- globalThis.synsetSimilarity = synsetSimilarity;
419
- globalThis.synsetSimilarityPreprocessed = synsetSimilarityPreprocessed;
420
- globalThis.areSynonyms = areSynonyms;
421
- globalThis.SYNONYM_GROUPS = SYNONYM_GROUPS;
422
- globalThis.textSimilarity = textSimilarity;
423
- globalThis.loadPreprocessedData = loadPreprocessedData;
424
- globalThis.getPreprocessedLang = getPreprocessedLang;
425
- globalThis.findBestMatchPreprocessed = findBestMatchPreprocessed;
426
- globalThis.tokenizeForSimilarity = tokenizeForSimilarity;
427
- }
 
1
+ // ============================================================
2
+ // Text Similarity — Thuật toán so khớp chuỗi
3
+ // Gồm 4 thuật toán: Levenshtein, Jaccard, Cosine, Synset
4
+ // + Preprocessed data support (TF-IDF, pre-tokenized)
5
+ // ============================================================
6
+
7
+ // --- Preprocessed data cache ---
8
+ // Loaded from data/preprocessed.json (build-time) hoặc localStorage (client)
9
+ var _preprocessedData = null;
10
+ var _PREPROCESSED_STORAGE_KEY = 'hikari_preprocessed';
11
+
12
+ /**
13
+ * Load preprocessed data.
14
+ * - Browser: thử localStorage trước, nếu không có hoặc outdated thì fetch từ server
15
+ * - Node/test: load trực tiếp từ file
16
+ * @returns {Promise<void>}
17
+ */
18
+ async function loadPreprocessedData() {
19
+ // Node/test environment
20
+ if (typeof module !== 'undefined' && module.exports) {
21
+ try {
22
+ var fs = require('fs');
23
+ var path = require('path');
24
+ var filePath = path.join(__dirname, '..', 'data', 'preprocessed.json');
25
+ _preprocessedData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
26
+ } catch (e) {
27
+ _preprocessedData = null;
28
+ }
29
+ return;
30
+ }
31
+
32
+ // Browser: thử localStorage
33
+ try {
34
+ var cached = localStorage.getItem(_PREPROCESSED_STORAGE_KEY);
35
+ if (cached) {
36
+ var parsed = JSON.parse(cached);
37
+ // Fetch version từ server để check outdated
38
+ var resp = await fetch('data/preprocessed.json', { method: 'HEAD' });
39
+ // Dùng Last-Modified hoặc so sánh version
40
+ if (parsed && parsed.version) {
41
+ _preprocessedData = parsed;
42
+ // Background check: fetch full file và so sánh version
43
+ fetch('data/preprocessed.json').then(function(r) { return r.json(); }).then(function(fresh) {
44
+ if (fresh.version !== parsed.version) {
45
+ _preprocessedData = fresh;
46
+ localStorage.setItem(_PREPROCESSED_STORAGE_KEY, JSON.stringify(fresh));
47
+ }
48
+ }).catch(function() {});
49
+ return;
50
+ }
51
+ }
52
+ } catch (e) { /* localStorage not available */ }
53
+
54
+ // Browser: fetch từ server
55
+ try {
56
+ var response = await fetch('data/preprocessed.json');
57
+ if (response.ok) {
58
+ _preprocessedData = await response.json();
59
+ try {
60
+ localStorage.setItem(_PREPROCESSED_STORAGE_KEY, JSON.stringify(_preprocessedData));
61
+ } catch (e) { /* quota exceeded */ }
62
+ }
63
+ } catch (e) {
64
+ _preprocessedData = null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Lấy preprocessed data cho ngôn ngữ hiện tại.
70
+ * @param {string} lang
71
+ * @returns {object|null} { idf, statements } hoặc null
72
+ */
73
+ function getPreprocessedLang(lang) {
74
+ if (!_preprocessedData || !_preprocessedData.langs) return null;
75
+ return _preprocessedData.langs[lang] || null;
76
+ }
77
+
78
+ // ---- Tokenize (dùng cho input mới) ----
79
+
80
+ /**
81
+ * Tokenize input text: lowercase, bỏ dấu (vi), bỏ punctuation, tách từ.
82
+ * Dùng cùng logic với scripts/preprocess.js để đảm bảo consistency.
83
+ * @param {string} text
84
+ * @param {string} lang
85
+ * @returns {string[]}
86
+ */
87
+ function tokenizeForSimilarity(text, lang) {
88
+ var s = String(text).toLowerCase();
89
+ if (lang === 'vi' && typeof removeVietnameseDiacritics === 'function') {
90
+ s = removeVietnameseDiacritics(s);
91
+ }
92
+ s = s.replace(/[?!.,;:"""''`~()[\]{}\\|@#$%^&]/g, ' ');
93
+ return s.split(/\s+/).filter(function(w) { return w.length > 0; });
94
+ }
95
+
96
+ // ---- 1. Levenshtein Distance ----
97
+
98
+ function levenshteinDistance(a, b) {
99
+ var strA = String(a);
100
+ var strB = String(b);
101
+ var lenA = strA.length;
102
+ var lenB = strB.length;
103
+
104
+ if (lenA === 0) return lenB;
105
+ if (lenB === 0) return lenA;
106
+
107
+ var prev = [];
108
+ var curr = [];
109
+ var i, j;
110
+
111
+ for (j = 0; j <= lenB; j++) prev[j] = j;
112
+
113
+ for (i = 1; i <= lenA; i++) {
114
+ curr[0] = i;
115
+ for (j = 1; j <= lenB; j++) {
116
+ if (strA[i - 1] === strB[j - 1]) {
117
+ curr[j] = prev[j - 1];
118
+ } else {
119
+ curr[j] = 1 + Math.min(prev[j - 1], prev[j], curr[j - 1]);
120
+ }
121
+ }
122
+ var tmp = prev;
123
+ prev = curr;
124
+ curr = tmp;
125
+ }
126
+ return prev[lenB];
127
+ }
128
+
129
+ // ---- 2. Jaccard Similarity ----
130
+
131
+ function jaccardSimilarity(a, b) {
132
+ var wordsA = String(a).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
133
+ var wordsB = String(b).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
134
+
135
+ if (wordsA.length === 0 && wordsB.length === 0) return 0;
136
+
137
+ var setA = {}, setB = {}, i;
138
+ for (i = 0; i < wordsA.length; i++) setA[wordsA[i]] = true;
139
+ for (i = 0; i < wordsB.length; i++) setB[wordsB[i]] = true;
140
+
141
+ var intersection = 0, union = {};
142
+ for (var k1 in setA) { union[k1] = true; if (setB[k1]) intersection++; }
143
+ for (var k2 in setB) { union[k2] = true; }
144
+
145
+ var unionSize = Object.keys(union).length;
146
+ return unionSize === 0 ? 0 : intersection / unionSize;
147
+ }
148
+
149
+ // ---- 3. Cosine Similarity ----
150
+
151
+ function cosineSimilarity(a, b) {
152
+ var wordsA = String(a).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
153
+ var wordsB = String(b).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
154
+
155
+ if (wordsA.length === 0 || wordsB.length === 0) return 0;
156
+
157
+ var tfA = {}, tfB = {}, i;
158
+ for (i = 0; i < wordsA.length; i++) tfA[wordsA[i]] = (tfA[wordsA[i]] || 0) + 1;
159
+ for (i = 0; i < wordsB.length; i++) tfB[wordsB[i]] = (tfB[wordsB[i]] || 0) + 1;
160
+
161
+ var vocab = {};
162
+ for (var ka in tfA) vocab[ka] = true;
163
+ for (var kb in tfB) vocab[kb] = true;
164
+
165
+ var dot = 0, magA = 0, magB = 0;
166
+ var keys = Object.keys(vocab);
167
+ for (i = 0; i < keys.length; i++) {
168
+ var va = tfA[keys[i]] || 0;
169
+ var vb = tfB[keys[i]] || 0;
170
+ dot += va * vb;
171
+ magA += va * va;
172
+ magB += vb * vb;
173
+ }
174
+
175
+ magA = Math.sqrt(magA);
176
+ magB = Math.sqrt(magB);
177
+ return (magA === 0 || magB === 0) ? 0 : dot / (magA * magB);
178
+ }
179
+
180
+ /**
181
+ * Cosine similarity sử dụng TF-IDF vectors (preprocessed).
182
+ * Input vector được tính on-the-fly, corpus vector đã preprocessed.
183
+ * @param {object} inputTFIDF - TF-IDF vector của input
184
+ * @param {number} inputMag - Magnitude của input vector
185
+ * @param {object} corpusTFIDF - TF-IDF vector đã preprocessed
186
+ * @param {number} corpusMag - Magnitude đã preprocessed
187
+ * @returns {number} [0, 1]
188
+ */
189
+ function cosineSimilarityTFIDF(inputTFIDF, inputMag, corpusTFIDF, corpusMag) {
190
+ if (inputMag === 0 || corpusMag === 0) return 0;
191
+
192
+ var dot = 0;
193
+ // Iterate over smaller vector for efficiency
194
+ for (var word in inputTFIDF) {
195
+ if (corpusTFIDF[word]) {
196
+ dot += inputTFIDF[word] * corpusTFIDF[word];
197
+ }
198
+ }
199
+ return dot / (inputMag * corpusMag);
200
+ }
201
+
202
+ // ---- 4. Synset Similarity ----
203
+
204
+ var SYNONYM_GROUPS = [
205
+ ['xin chao','chao','hi','hello','hey','yo'],
206
+ ['tam biet','bye','goodbye','hen gap lai','tot lanh'],
207
+ ['cam on','thanks','thank','thank you'],
208
+ ['ten','name','ai','who','la ai'],
209
+ ['lam gi','lam duoc','co the','giup','help','what can'],
210
+ ['may gio','gio','time','clock','bao gio'],
211
+ ['ngay','hom nay','date','today','ngay may'],
212
+ ['thu','thu may','day','what day'],
213
+ ['tinh','calculate','math','cong','tru','nhan','chia','plus','minus'],
214
+ ['doi','convert','sang','to','chuyen doi'],
215
+ ['chatbot','bot','ai','robot','may','machine'],
216
+ ['la gi','what is','what','gi','mean'],
217
+ ['khoe','vui','happy','fine','good','ok'],
218
+ ['tuoi','age','old','bao nhieu tuoi'],
219
+ ['o dau','where','dau','location'],
220
+ ['thich','like','love','yeu'],
221
+ ['huong dan','cach','how','guide','help','su dung','use']
222
+ ];
223
+
224
+ var _synonymLookup = null;
225
+ function _getSynonymLookup() {
226
+ if (_synonymLookup) return _synonymLookup;
227
+ _synonymLookup = {};
228
+ for (var g = 0; g < SYNONYM_GROUPS.length; g++) {
229
+ for (var w = 0; w < SYNONYM_GROUPS[g].length; w++) {
230
+ var word = SYNONYM_GROUPS[g][w].toLowerCase();
231
+ if (!_synonymLookup[word]) _synonymLookup[word] = [];
232
+ _synonymLookup[word].push(g);
233
+ }
234
+ }
235
+ return _synonymLookup;
236
+ }
237
+
238
+ function areSynonyms(wordA, wordB) {
239
+ if (wordA === wordB) return true;
240
+ var lookup = _getSynonymLookup();
241
+ var groupsA = lookup[wordA];
242
+ var groupsB = lookup[wordB];
243
+ if (!groupsA || !groupsB) return false;
244
+ for (var i = 0; i < groupsA.length; i++) {
245
+ for (var j = 0; j < groupsB.length; j++) {
246
+ if (groupsA[i] === groupsB[j]) return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+
252
+ function synsetSimilarity(a, b) {
253
+ var wordsA = String(a).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
254
+ var wordsB = String(b).toLowerCase().trim().split(/\s+/).filter(function(w) { return w.length > 0; });
255
+
256
+ if (wordsA.length === 0 || wordsB.length === 0) return 0;
257
+
258
+ var matchAtoB = 0;
259
+ for (var i = 0; i < wordsA.length; i++) {
260
+ for (var j = 0; j < wordsB.length; j++) {
261
+ if (areSynonyms(wordsA[i], wordsB[j])) { matchAtoB++; break; }
262
+ }
263
+ }
264
+ var matchBtoA = 0;
265
+ for (var m = 0; m < wordsB.length; m++) {
266
+ for (var n = 0; n < wordsA.length; n++) {
267
+ if (areSynonyms(wordsB[m], wordsA[n])) { matchBtoA++; break; }
268
+ }
269
+ }
270
+
271
+ return ((matchAtoB / wordsA.length) + (matchBtoA / wordsB.length)) / 2;
272
+ }
273
+
274
+ /**
275
+ * Synset similarity sử dụng preprocessed synonym group indices.
276
+ * So sánh overlap giữa synGroups của input và corpus statement.
277
+ * @param {number[]} inputGroups - Synonym group indices của input
278
+ * @param {number[]} corpusGroups - Synonym group indices đã preprocessed
279
+ * @returns {number} [0, 1]
280
+ */
281
+ function synsetSimilarityPreprocessed(inputGroups, corpusGroups) {
282
+ if (inputGroups.length === 0 && corpusGroups.length === 0) return 0;
283
+ if (inputGroups.length === 0 || corpusGroups.length === 0) return 0;
284
+
285
+ var setB = {};
286
+ for (var j = 0; j < corpusGroups.length; j++) setB[corpusGroups[j]] = true;
287
+
288
+ var overlap = 0;
289
+ for (var i = 0; i < inputGroups.length; i++) {
290
+ if (setB[inputGroups[i]]) overlap++;
291
+ }
292
+
293
+ var total = {};
294
+ for (var a = 0; a < inputGroups.length; a++) total[inputGroups[a]] = true;
295
+ for (var b = 0; b < corpusGroups.length; b++) total[corpusGroups[b]] = true;
296
+ var unionSize = Object.keys(total).length;
297
+
298
+ return unionSize === 0 ? 0 : overlap / unionSize;
299
+ }
300
+
301
+ // ---- Combined: textSimilarity (general purpose) ----
302
+
303
+ function textSimilarity(a, b) {
304
+ var strA = String(a);
305
+ var strB = String(b);
306
+
307
+ if (strA === strB && strA.length > 0) return 1.0;
308
+ if (strA.length === 0 && strB.length === 0) return 0;
309
+
310
+ var maxLen = Math.max(strA.length, strB.length);
311
+ var levSim = maxLen > 0 ? 1 - (levenshteinDistance(strA, strB) / maxLen) : 0;
312
+ var jacSim = jaccardSimilarity(strA, strB);
313
+ var cosSim = cosineSimilarity(strA, strB);
314
+ var synSim = synsetSimilarity(strA, strB);
315
+
316
+ return Math.max(0, Math.min(1, (levSim + jacSim + cosSim + synSim) / 4));
317
+ }
318
+
319
+ // ---- Optimized: findBestMatchPreprocessed ----
320
+
321
+ /**
322
+ * Tìm câu trả lời phù hợp nhất từ preprocessed data.
323
+ * Sử dụng TF-IDF cosine + synset preprocessed + Jaccard + Levenshtein.
324
+ * Nhanh hơn textSimilarity() vì corpus đã được tiền xử lý.
325
+ *
326
+ * @param {string} input - Input người dùng (raw)
327
+ * @param {string} lang - Ngôn ngữ hiện tại
328
+ * @param {number} threshold - Ngưỡng similarity (mặc định 0.3)
329
+ * @returns {{answer: string, score: number}|null} Kết quả hoặc null
330
+ */
331
+ function findBestMatchPreprocessed(input, lang, threshold) {
332
+ threshold = threshold || 0.3;
333
+ var ppLang = getPreprocessedLang(lang);
334
+ if (!ppLang) return null; // Fallback: caller sẽ dùng textSimilarity thường
335
+
336
+ var idf = ppLang.idf;
337
+ var statements = ppLang.statements;
338
+
339
+ // Tokenize input
340
+ var inputTokens = tokenizeForSimilarity(input, lang);
341
+ if (inputTokens.length === 0) return null;
342
+
343
+ // Build input TF
344
+ var inputTF = {};
345
+ for (var t = 0; t < inputTokens.length; t++) {
346
+ inputTF[inputTokens[t]] = (inputTF[inputTokens[t]] || 0) + 1;
347
+ }
348
+
349
+ // Build input TF-IDF
350
+ var inputTFIDF = {};
351
+ for (var word in inputTF) {
352
+ inputTFIDF[word] = inputTF[word] * (idf[word] || 1);
353
+ }
354
+ var inputMag = 0;
355
+ for (var w in inputTFIDF) inputMag += inputTFIDF[w] * inputTFIDF[w];
356
+ inputMag = Math.sqrt(inputMag);
357
+
358
+ // Build input synonym groups
359
+ var lookup = _getSynonymLookup();
360
+ var inputSynGroups = [];
361
+ var seenGroups = {};
362
+ for (var s = 0; s < inputTokens.length; s++) {
363
+ var groups = lookup[inputTokens[s]];
364
+ if (groups) {
365
+ for (var gi = 0; gi < groups.length; gi++) {
366
+ if (!seenGroups[groups[gi]]) {
367
+ inputSynGroups.push(groups[gi]);
368
+ seenGroups[groups[gi]] = true;
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ // Input normalized string (for Levenshtein/Jaccard)
375
+ var inputNorm = inputTokens.join(' ');
376
+
377
+ // Compare with all preprocessed statements
378
+ var bestScore = -1;
379
+ var bestAnswer = null;
380
+
381
+ for (var i = 0; i < statements.length; i++) {
382
+ var stmt = statements[i];
383
+ var stmtNorm = stmt.tokens.join(' ');
384
+
385
+ // 1. Levenshtein similarity
386
+ var maxLen = Math.max(inputNorm.length, stmtNorm.length);
387
+ var levSim = maxLen > 0 ? 1 - (levenshteinDistance(inputNorm, stmtNorm) / maxLen) : 0;
388
+
389
+ // 2. Jaccard (on tokens)
390
+ var jacSim = jaccardSimilarity(inputNorm, stmtNorm);
391
+
392
+ // 3. TF-IDF Cosine (preprocessed)
393
+ var cosSim = cosineSimilarityTFIDF(inputTFIDF, inputMag, stmt.tfidf, stmt.magnitude);
394
+
395
+ // 4. Synset (preprocessed)
396
+ var synSim = synsetSimilarityPreprocessed(inputSynGroups, stmt.synGroups);
397
+
398
+ var score = (levSim + jacSim + cosSim + synSim) / 4;
399
+
400
+ if (score > bestScore) {
401
+ bestScore = score;
402
+ bestAnswer = stmt.answer;
403
+ }
404
+ }
405
+
406
+ if (bestScore >= threshold) {
407
+ return { answer: bestAnswer, score: bestScore };
408
+ }
409
+ return null;
410
+ }
411
+
412
+ // Node/test: export to globalThis
413
+ if (typeof module !== 'undefined' && module.exports) {
414
+ globalThis.levenshteinDistance = levenshteinDistance;
415
+ globalThis.jaccardSimilarity = jaccardSimilarity;
416
+ globalThis.cosineSimilarity = cosineSimilarity;
417
+ globalThis.cosineSimilarityTFIDF = cosineSimilarityTFIDF;
418
+ globalThis.synsetSimilarity = synsetSimilarity;
419
+ globalThis.synsetSimilarityPreprocessed = synsetSimilarityPreprocessed;
420
+ globalThis.areSynonyms = areSynonyms;
421
+ globalThis.SYNONYM_GROUPS = SYNONYM_GROUPS;
422
+ globalThis.textSimilarity = textSimilarity;
423
+ globalThis.loadPreprocessedData = loadPreprocessedData;
424
+ globalThis.getPreprocessedLang = getPreprocessedLang;
425
+ globalThis.findBestMatchPreprocessed = findBestMatchPreprocessed;
426
+ globalThis.tokenizeForSimilarity = tokenizeForSimilarity;
427
+ }
adapters/time-adapter.js CHANGED
@@ -1,89 +1,89 @@
1
- // ============================================================
2
- // Time Adapter — Trả lời câu hỏi thời gian/ngày tháng/thứ
3
- // Phụ thuộc: currentLang, _adapterPath
4
- // ============================================================
5
-
6
- var TIME_KEYWORDS = {
7
- vi: { time: ['mấy giờ', 'giờ', 'thời gian'], date: ['ngày mấy', 'hôm nay', 'ngày tháng'], day: ['thứ mấy', 'thứ'] },
8
- en: { time: ['what time', 'time', 'clock'], date: ['what date', 'today', 'date'], day: ['what day', 'day'] },
9
- ja: { time: ['何時', '今何時', '時間'], date: ['今日', '何日', '日付'], day: ['何曜日', '曜日'] }
10
- };
11
-
12
- var DAY_NAMES = {
13
- vi: ['Chủ Nhật', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy'],
14
- en: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
15
- ja: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日']
16
- };
17
-
18
- var MONTH_NAMES_EN = [
19
- 'January', 'February', 'March', 'April', 'May', 'June',
20
- 'July', 'August', 'September', 'October', 'November', 'December'
21
- ];
22
-
23
- function timeAdapter(rs, args) {
24
- _adapterPath.push('time_adapter');
25
- var input = (args || []).join(' ').trim().toLowerCase();
26
- var lang = currentLang || 'vi';
27
- var keywords = TIME_KEYWORDS[lang] || TIME_KEYWORDS['vi'];
28
- var now = new Date();
29
-
30
- for (var i = 0; i < keywords.time.length; i++) {
31
- if (input.indexOf(keywords.time[i].toLowerCase()) !== -1) return formatTime(now, lang);
32
- }
33
- for (var j = 0; j < keywords.date.length; j++) {
34
- if (input.indexOf(keywords.date[j].toLowerCase()) !== -1) return formatDate(now, lang);
35
- }
36
- for (var k = 0; k < keywords.day.length; k++) {
37
- if (input.indexOf(keywords.day[k].toLowerCase()) !== -1) return formatDay(now, lang);
38
- }
39
-
40
- if (lang === 'en') return "I don't understand your time request.";
41
- if (lang === 'ja') return '時間に関するリクエストが理解できませんでした。';
42
- return 'Mình không hiểu yêu cầu về thời gian của bạn.';
43
- }
44
-
45
- function formatTime(date, lang) {
46
- var hours = date.getHours();
47
- var minutes = date.getMinutes();
48
- var mm = minutes < 10 ? '0' + minutes : String(minutes);
49
-
50
- if (lang === 'en') {
51
- var period = hours >= 12 ? 'PM' : 'AM';
52
- var h12 = hours % 12;
53
- if (h12 === 0) h12 = 12;
54
- return h12 + ':' + mm + ' ' + period;
55
- } else if (lang === 'ja') {
56
- return hours + '時' + mm + '分';
57
- }
58
- var hh = hours < 10 ? '0' + hours : String(hours);
59
- return hh + ':' + mm;
60
- }
61
-
62
- function formatDate(date, lang) {
63
- var day = date.getDate();
64
- var month = date.getMonth();
65
- var year = date.getFullYear();
66
-
67
- if (lang === 'en') return MONTH_NAMES_EN[month] + ' ' + day + ', ' + year;
68
- if (lang === 'ja') return year + '年' + (month + 1) + '月' + day + '日';
69
- var dd = day < 10 ? '0' + day : String(day);
70
- var mmStr = (month + 1) < 10 ? '0' + (month + 1) : String(month + 1);
71
- return dd + '/' + mmStr + '/' + year;
72
- }
73
-
74
- function formatDay(date, lang) {
75
- var dayIndex = date.getDay();
76
- var names = DAY_NAMES[lang] || DAY_NAMES['vi'];
77
- return names[dayIndex];
78
- }
79
-
80
- // Node/test: export to globalThis
81
- if (typeof module !== 'undefined' && module.exports) {
82
- globalThis.TIME_KEYWORDS = TIME_KEYWORDS;
83
- globalThis.DAY_NAMES = DAY_NAMES;
84
- globalThis.MONTH_NAMES_EN = MONTH_NAMES_EN;
85
- globalThis.timeAdapter = timeAdapter;
86
- globalThis.formatTime = formatTime;
87
- globalThis.formatDate = formatDate;
88
- globalThis.formatDay = formatDay;
89
- }
 
1
+ // ============================================================
2
+ // Time Adapter — Trả lời câu hỏi thời gian/ngày tháng/thứ
3
+ // Phụ thuộc: currentLang, _adapterPath
4
+ // ============================================================
5
+
6
+ var TIME_KEYWORDS = {
7
+ vi: { time: ['mấy giờ', 'giờ', 'thời gian'], date: ['ngày mấy', 'hôm nay', 'ngày tháng'], day: ['thứ mấy', 'thứ'] },
8
+ en: { time: ['what time', 'time', 'clock'], date: ['what date', 'today', 'date'], day: ['what day', 'day'] },
9
+ ja: { time: ['何時', '今何時', '時間'], date: ['今日', '何日', '日付'], day: ['何曜日', '曜日'] }
10
+ };
11
+
12
+ var DAY_NAMES = {
13
+ vi: ['Chủ Nhật', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy'],
14
+ en: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
15
+ ja: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日']
16
+ };
17
+
18
+ var MONTH_NAMES_EN = [
19
+ 'January', 'February', 'March', 'April', 'May', 'June',
20
+ 'July', 'August', 'September', 'October', 'November', 'December'
21
+ ];
22
+
23
+ function timeAdapter(rs, args) {
24
+ _adapterPath.push('time_adapter');
25
+ var input = (args || []).join(' ').trim().toLowerCase();
26
+ var lang = currentLang || 'vi';
27
+ var keywords = TIME_KEYWORDS[lang] || TIME_KEYWORDS['vi'];
28
+ var now = new Date();
29
+
30
+ for (var i = 0; i < keywords.time.length; i++) {
31
+ if (input.indexOf(keywords.time[i].toLowerCase()) !== -1) return formatTime(now, lang);
32
+ }
33
+ for (var j = 0; j < keywords.date.length; j++) {
34
+ if (input.indexOf(keywords.date[j].toLowerCase()) !== -1) return formatDate(now, lang);
35
+ }
36
+ for (var k = 0; k < keywords.day.length; k++) {
37
+ if (input.indexOf(keywords.day[k].toLowerCase()) !== -1) return formatDay(now, lang);
38
+ }
39
+
40
+ if (lang === 'en') return "I don't understand your time request.";
41
+ if (lang === 'ja') return '時間に関するリクエストが理解できませんでした。';
42
+ return 'Mình không hiểu yêu cầu về thời gian của bạn.';
43
+ }
44
+
45
+ function formatTime(date, lang) {
46
+ var hours = date.getHours();
47
+ var minutes = date.getMinutes();
48
+ var mm = minutes < 10 ? '0' + minutes : String(minutes);
49
+
50
+ if (lang === 'en') {
51
+ var period = hours >= 12 ? 'PM' : 'AM';
52
+ var h12 = hours % 12;
53
+ if (h12 === 0) h12 = 12;
54
+ return h12 + ':' + mm + ' ' + period;
55
+ } else if (lang === 'ja') {
56
+ return hours + '時' + mm + '分';
57
+ }
58
+ var hh = hours < 10 ? '0' + hours : String(hours);
59
+ return hh + ':' + mm;
60
+ }
61
+
62
+ function formatDate(date, lang) {
63
+ var day = date.getDate();
64
+ var month = date.getMonth();
65
+ var year = date.getFullYear();
66
+
67
+ if (lang === 'en') return MONTH_NAMES_EN[month] + ' ' + day + ', ' + year;
68
+ if (lang === 'ja') return year + '年' + (month + 1) + '月' + day + '日';
69
+ var dd = day < 10 ? '0' + day : String(day);
70
+ var mmStr = (month + 1) < 10 ? '0' + (month + 1) : String(month + 1);
71
+ return dd + '/' + mmStr + '/' + year;
72
+ }
73
+
74
+ function formatDay(date, lang) {
75
+ var dayIndex = date.getDay();
76
+ var names = DAY_NAMES[lang] || DAY_NAMES['vi'];
77
+ return names[dayIndex];
78
+ }
79
+
80
+ // Node/test: export to globalThis
81
+ if (typeof module !== 'undefined' && module.exports) {
82
+ globalThis.TIME_KEYWORDS = TIME_KEYWORDS;
83
+ globalThis.DAY_NAMES = DAY_NAMES;
84
+ globalThis.MONTH_NAMES_EN = MONTH_NAMES_EN;
85
+ globalThis.timeAdapter = timeAdapter;
86
+ globalThis.formatTime = formatTime;
87
+ globalThis.formatDate = formatDate;
88
+ globalThis.formatDay = formatDay;
89
+ }
adapters/unit-conversion.js CHANGED
@@ -1,145 +1,145 @@
1
- // ============================================================
2
- // Unit Conversion Adapter — Chuyển đổi đơn vị đo lường
3
- // Phụ thuộc: currentLang, _adapterPath
4
- // ============================================================
5
-
6
- var CONVERSION_FACTORS = {
7
- length: { m: 1, km: 1000, cm: 0.01, mm: 0.001, mile: 1609.344, yard: 0.9144, foot: 0.3048, inch: 0.0254 },
8
- mass: { kg: 1, g: 0.001, mg: 0.000001, pound: 0.453592, ounce: 0.0283495 }
9
- };
10
-
11
- var TEMPERATURE_UNITS = {
12
- celsius: 'celsius', c: 'celsius',
13
- fahrenheit: 'fahrenheit', f: 'fahrenheit',
14
- kelvin: 'kelvin', k: 'kelvin'
15
- };
16
-
17
- function convertTemperature(value, from, to) {
18
- if (from === to) return value;
19
- var celsius;
20
- if (from === 'celsius') celsius = value;
21
- else if (from === 'fahrenheit') celsius = (value - 32) * 5 / 9;
22
- else celsius = value - 273.15;
23
-
24
- if (to === 'celsius') return celsius;
25
- if (to === 'fahrenheit') return (celsius * 9 / 5) + 32;
26
- return celsius + 273.15;
27
- }
28
-
29
- function convertUnit(value, fromUnit, toUnit) {
30
- var from = String(fromUnit).toLowerCase().trim();
31
- var to = String(toUnit).toLowerCase().trim();
32
- if (from === to) return value;
33
-
34
- var fromTemp = TEMPERATURE_UNITS[from];
35
- var toTemp = TEMPERATURE_UNITS[to];
36
- if (fromTemp && toTemp) return convertTemperature(value, fromTemp, toTemp);
37
- if (fromTemp || toTemp) return null;
38
-
39
- var categories = Object.keys(CONVERSION_FACTORS);
40
- for (var i = 0; i < categories.length; i++) {
41
- var factors = CONVERSION_FACTORS[categories[i]];
42
- if (factors[from] !== undefined && factors[to] !== undefined) {
43
- return value * (factors[from] / factors[to]);
44
- }
45
- }
46
- return null;
47
- }
48
-
49
- function parseConversionRequest(input, lang) {
50
- if (typeof input !== 'string' || input.trim().length === 0) return null;
51
- var text = input.trim();
52
-
53
- var separators;
54
- if (lang === 'ja') separators = ['に変換', 'は何', 'を'];
55
- else if (lang === 'en') separators = ['convert', ' to ', ' in '];
56
- else separators = [' sang ', ' ra '];
57
-
58
- for (var i = 0; i < separators.length; i++) {
59
- var sep = separators[i];
60
- var idx = text.toLowerCase().indexOf(sep.toLowerCase());
61
- if (idx === -1) continue;
62
-
63
- var leftPart = text.substring(0, idx).trim();
64
- var rightPart = text.substring(idx + sep.length).trim();
65
- if (leftPart.length === 0 || rightPart.length === 0) continue;
66
-
67
- var leftMatch = leftPart.match(/(-?\d+(?:\.\d+)?)\s*(.+)/);
68
- if (!leftMatch) continue;
69
-
70
- var value = parseFloat(leftMatch[1]);
71
- var fromUnit = leftMatch[2].trim().toLowerCase();
72
- var toUnit = rightPart.trim().toLowerCase();
73
- if (isNaN(value)) continue;
74
-
75
- return { value: value, from: fromUnit, to: toUnit };
76
- }
77
- return null;
78
- }
79
-
80
- function getSupportedUnits() {
81
- var units = [];
82
- var categories = Object.keys(CONVERSION_FACTORS);
83
- for (var i = 0; i < categories.length; i++) {
84
- units = units.concat(Object.keys(CONVERSION_FACTORS[categories[i]]));
85
- }
86
- units = units.concat(['celsius', 'fahrenheit', 'kelvin', 'c', 'f', 'k']);
87
- return units.join(', ');
88
- }
89
-
90
- function unitConversionAdapter(rs, args) {
91
- _adapterPath.push('unit_conversion');
92
- var input = (args || []).join(' ').trim();
93
- var lang = currentLang || 'vi';
94
-
95
- if (input.length === 0) {
96
- if (lang === 'en') return 'Please provide a conversion request (e.g., "5 km to m").';
97
- if (lang === 'ja') return '変換リクエストを入力してください(例: 「5 km を m」)。';
98
- return 'Vui lòng nhập yêu cầu chuyển đổi (ví dụ: "5 km sang m").';
99
- }
100
-
101
- var parsed = parseConversionRequest(input, lang);
102
- if (!parsed) {
103
- if (lang === 'en') return 'Invalid syntax. Example: "5 km to m", "100 fahrenheit to celsius".';
104
- if (lang === 'ja') return '構文が無効です。例: 「5 km を m」、「100 fahrenheit を celsius」。';
105
- return 'Cú pháp không hợp lệ. Ví dụ: "5 km sang m", "100 fahrenheit sang celsius".';
106
- }
107
-
108
- var allUnits = {};
109
- var categories = Object.keys(CONVERSION_FACTORS);
110
- for (var i = 0; i < categories.length; i++) {
111
- var catUnits = Object.keys(CONVERSION_FACTORS[categories[i]]);
112
- for (var j = 0; j < catUnits.length; j++) allUnits[catUnits[j]] = true;
113
- }
114
- var tempKeys = Object.keys(TEMPERATURE_UNITS);
115
- for (var t = 0; t < tempKeys.length; t++) allUnits[tempKeys[t]] = true;
116
-
117
- if (!allUnits[parsed.from.toLowerCase()] || !allUnits[parsed.to.toLowerCase()]) {
118
- var supported = getSupportedUnits();
119
- if (lang === 'en') return 'Unsupported unit. Supported units: ' + supported;
120
- if (lang === 'ja') return 'サポートされていない単位です。対応単位: ' + supported;
121
- return 'Đơn vị kh��ng được hỗ trợ. Các đơn vị hỗ trợ: ' + supported;
122
- }
123
-
124
- var result = convertUnit(parsed.value, parsed.from.toLowerCase(), parsed.to.toLowerCase());
125
- if (result === null) {
126
- var supported2 = getSupportedUnits();
127
- if (lang === 'en') return 'Cannot convert between incompatible units. Supported units: ' + supported2;
128
- if (lang === 'ja') return '互換性のない単位間の変換はできません。対応単位: ' + supported2;
129
- return 'Không thể chuyển đổi giữa các đơn vị không tương thích. Các đơn vị hỗ trợ: ' + supported2;
130
- }
131
-
132
- var resultStr = Number.isInteger(result) ? String(result) : result.toFixed(4).replace(/\.?0+$/, '');
133
- return parsed.value + ' ' + parsed.from + ' = ' + resultStr + ' ' + parsed.to;
134
- }
135
-
136
- // Node/test: export to globalThis
137
- if (typeof module !== 'undefined' && module.exports) {
138
- globalThis.CONVERSION_FACTORS = CONVERSION_FACTORS;
139
- globalThis.TEMPERATURE_UNITS = TEMPERATURE_UNITS;
140
- globalThis.convertTemperature = convertTemperature;
141
- globalThis.convertUnit = convertUnit;
142
- globalThis.parseConversionRequest = parseConversionRequest;
143
- globalThis.getSupportedUnits = getSupportedUnits;
144
- globalThis.unitConversionAdapter = unitConversionAdapter;
145
- }
 
1
+ // ============================================================
2
+ // Unit Conversion Adapter — Chuyển đổi đơn vị đo lường
3
+ // Phụ thuộc: currentLang, _adapterPath
4
+ // ============================================================
5
+
6
+ var CONVERSION_FACTORS = {
7
+ length: { m: 1, km: 1000, cm: 0.01, mm: 0.001, mile: 1609.344, yard: 0.9144, foot: 0.3048, inch: 0.0254 },
8
+ mass: { kg: 1, g: 0.001, mg: 0.000001, pound: 0.453592, ounce: 0.0283495 }
9
+ };
10
+
11
+ var TEMPERATURE_UNITS = {
12
+ celsius: 'celsius', c: 'celsius',
13
+ fahrenheit: 'fahrenheit', f: 'fahrenheit',
14
+ kelvin: 'kelvin', k: 'kelvin'
15
+ };
16
+
17
+ function convertTemperature(value, from, to) {
18
+ if (from === to) return value;
19
+ var celsius;
20
+ if (from === 'celsius') celsius = value;
21
+ else if (from === 'fahrenheit') celsius = (value - 32) * 5 / 9;
22
+ else celsius = value - 273.15;
23
+
24
+ if (to === 'celsius') return celsius;
25
+ if (to === 'fahrenheit') return (celsius * 9 / 5) + 32;
26
+ return celsius + 273.15;
27
+ }
28
+
29
+ function convertUnit(value, fromUnit, toUnit) {
30
+ var from = String(fromUnit).toLowerCase().trim();
31
+ var to = String(toUnit).toLowerCase().trim();
32
+ if (from === to) return value;
33
+
34
+ var fromTemp = TEMPERATURE_UNITS[from];
35
+ var toTemp = TEMPERATURE_UNITS[to];
36
+ if (fromTemp && toTemp) return convertTemperature(value, fromTemp, toTemp);
37
+ if (fromTemp || toTemp) return null;
38
+
39
+ var categories = Object.keys(CONVERSION_FACTORS);
40
+ for (var i = 0; i < categories.length; i++) {
41
+ var factors = CONVERSION_FACTORS[categories[i]];
42
+ if (factors[from] !== undefined && factors[to] !== undefined) {
43
+ return value * (factors[from] / factors[to]);
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function parseConversionRequest(input, lang) {
50
+ if (typeof input !== 'string' || input.trim().length === 0) return null;
51
+ var text = input.trim();
52
+
53
+ var separators;
54
+ if (lang === 'ja') separators = ['に変換', 'は何', 'を'];
55
+ else if (lang === 'en') separators = ['convert', ' to ', ' in '];
56
+ else separators = [' sang ', ' ra '];
57
+
58
+ for (var i = 0; i < separators.length; i++) {
59
+ var sep = separators[i];
60
+ var idx = text.toLowerCase().indexOf(sep.toLowerCase());
61
+ if (idx === -1) continue;
62
+
63
+ var leftPart = text.substring(0, idx).trim();
64
+ var rightPart = text.substring(idx + sep.length).trim();
65
+ if (leftPart.length === 0 || rightPart.length === 0) continue;
66
+
67
+ var leftMatch = leftPart.match(/(-?\d+(?:\.\d+)?)\s*(.+)/);
68
+ if (!leftMatch) continue;
69
+
70
+ var value = parseFloat(leftMatch[1]);
71
+ var fromUnit = leftMatch[2].trim().toLowerCase();
72
+ var toUnit = rightPart.trim().toLowerCase();
73
+ if (isNaN(value)) continue;
74
+
75
+ return { value: value, from: fromUnit, to: toUnit };
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function getSupportedUnits() {
81
+ var units = [];
82
+ var categories = Object.keys(CONVERSION_FACTORS);
83
+ for (var i = 0; i < categories.length; i++) {
84
+ units = units.concat(Object.keys(CONVERSION_FACTORS[categories[i]]));
85
+ }
86
+ units = units.concat(['celsius', 'fahrenheit', 'kelvin', 'c', 'f', 'k']);
87
+ return units.join(', ');
88
+ }
89
+
90
+ function unitConversionAdapter(rs, args) {
91
+ _adapterPath.push('unit_conversion');
92
+ var input = (args || []).join(' ').trim();
93
+ var lang = currentLang || 'vi';
94
+
95
+ if (input.length === 0) {
96
+ if (lang === 'en') return 'Please provide a conversion request (e.g., "5 km to m").';
97
+ if (lang === 'ja') return '変換リクエストを入力してください(例: 「5 km を m」)。';
98
+ return 'Vui lòng nhập yêu cầu chuyển đổi (ví dụ: "5 km sang m").';
99
+ }
100
+
101
+ var parsed = parseConversionRequest(input, lang);
102
+ if (!parsed) {
103
+ if (lang === 'en') return 'Invalid syntax. Example: "5 km to m", "100 fahrenheit to celsius".';
104
+ if (lang === 'ja') return '構文が無効です。例: 「5 km を m」、「100 fahrenheit を celsius」。';
105
+ return 'Cú pháp không hợp lệ. Ví dụ: "5 km sang m", "100 fahrenheit sang celsius".';
106
+ }
107
+
108
+ var allUnits = {};
109
+ var categories = Object.keys(CONVERSION_FACTORS);
110
+ for (var i = 0; i < categories.length; i++) {
111
+ var catUnits = Object.keys(CONVERSION_FACTORS[categories[i]]);
112
+ for (var j = 0; j < catUnits.length; j++) allUnits[catUnits[j]] = true;
113
+ }
114
+ var tempKeys = Object.keys(TEMPERATURE_UNITS);
115
+ for (var t = 0; t < tempKeys.length; t++) allUnits[tempKeys[t]] = true;
116
+
117
+ if (!allUnits[parsed.from.toLowerCase()] || !allUnits[parsed.to.toLowerCase()]) {
118
+ var supported = getSupportedUnits();
119
+ if (lang === 'en') return 'Unsupported unit. Supported units: ' + supported;
120
+ if (lang === 'ja') return 'サポートされていない単位です。対応単位: ' + supported;
121
+ return 'Đơn vị không được hỗ trợ. Các đơn vị hỗ trợ: ' + supported;
122
+ }
123
+
124
+ var result = convertUnit(parsed.value, parsed.from.toLowerCase(), parsed.to.toLowerCase());
125
+ if (result === null) {
126
+ var supported2 = getSupportedUnits();
127
+ if (lang === 'en') return 'Cannot convert between incompatible units. Supported units: ' + supported2;
128
+ if (lang === 'ja') return '互換性のない単位間の変換はできません。対応単位: ' + supported2;
129
+ return 'Không thể chuyển đổi giữa các đơn vị không tương thích. Các đơn vị hỗ trợ: ' + supported2;
130
+ }
131
+
132
+ var resultStr = Number.isInteger(result) ? String(result) : result.toFixed(4).replace(/\.?0+$/, '');
133
+ return parsed.value + ' ' + parsed.from + ' = ' + resultStr + ' ' + parsed.to;
134
+ }
135
+
136
+ // Node/test: export to globalThis
137
+ if (typeof module !== 'undefined' && module.exports) {
138
+ globalThis.CONVERSION_FACTORS = CONVERSION_FACTORS;
139
+ globalThis.TEMPERATURE_UNITS = TEMPERATURE_UNITS;
140
+ globalThis.convertTemperature = convertTemperature;
141
+ globalThis.convertUnit = convertUnit;
142
+ globalThis.parseConversionRequest = parseConversionRequest;
143
+ globalThis.getSupportedUnits = getSupportedUnits;
144
+ globalThis.unitConversionAdapter = unitConversionAdapter;
145
+ }
adapters/voice-adapter.js ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // Voice Adapter — STT (SpeechRecognition) + TTS (SpeechSynthesis)
3
+ // Sử dụng Web Speech API native của trình duyệt, không cần server.
4
+ // ============================================================
5
+
6
+ /**
7
+ * Ánh xạ ngôn ngữ app → locale Web Speech API.
8
+ */
9
+ var SPEECH_LOCALE_MAP = {
10
+ vi: 'vi-VN',
11
+ en: 'en-US',
12
+ ja: 'ja-JP'
13
+ };
14
+
15
+ // === Trạng thái nội bộ ===
16
+ var _recognition = null; // SpeechRecognition instance hiện tại
17
+ var _isListening = false; // Đang lắng nghe STT
18
+ var _sttSupported = false; // Browser hỗ trợ STT
19
+ var _ttsSupported = false; // Browser hỗ trợ TTS
20
+
21
+ /**
22
+ * Khởi tạo Voice Adapter — kiểm tra browser support.
23
+ * Gọi một lần khi app khởi động.
24
+ * @returns {{ sttSupported: boolean, ttsSupported: boolean }}
25
+ */
26
+ function initVoiceAdapter() {
27
+ _sttSupported = !!(
28
+ typeof window !== 'undefined' &&
29
+ (window.SpeechRecognition || window.webkitSpeechRecognition)
30
+ );
31
+ _ttsSupported = !!(
32
+ typeof window !== 'undefined' &&
33
+ window.speechSynthesis
34
+ );
35
+ return { sttSupported: _sttSupported, ttsSupported: _ttsSupported };
36
+ }
37
+
38
+ // ============================================================
39
+ // TTS — Text to Speech
40
+ // ============================================================
41
+
42
+ /**
43
+ * Lấy danh sách voices phù hợp với ngôn ngữ, ưu tiên localService.
44
+ * @param {string} lang - 'vi' | 'en' | 'ja'
45
+ * @returns {SpeechSynthesisVoice[]}
46
+ */
47
+ function getVoicesForLang(lang) {
48
+ if (!_ttsSupported) return [];
49
+ var locale = SPEECH_LOCALE_MAP[lang] || lang;
50
+ var prefix = locale.split('-')[0].toLowerCase(); // 'vi', 'en', 'ja'
51
+ var all = window.speechSynthesis.getVoices();
52
+ var filtered = all.filter(function (v) {
53
+ var vLang = (v.lang || '').toLowerCase();
54
+ return vLang === locale.toLowerCase() || vLang.startsWith(prefix + '-') || vLang.startsWith(prefix + '_');
55
+ });
56
+ // Ưu tiên localService (giọng tự nhiên cài sẵn)
57
+ filtered.sort(function (a, b) {
58
+ if (a.localService && !b.localService) return -1;
59
+ if (!a.localService && b.localService) return 1;
60
+ return 0;
61
+ });
62
+ return filtered;
63
+ }
64
+
65
+ /**
66
+ * Lấy voice mặc định tốt nhất cho ngôn ngữ.
67
+ * Ưu tiên localService, rồi lấy voice đầu tiên trong danh sách.
68
+ * @param {string} lang
69
+ * @returns {SpeechSynthesisVoice|null}
70
+ */
71
+ function getDefaultVoice(lang) {
72
+ var voices = getVoicesForLang(lang);
73
+ return voices.length > 0 ? voices[0] : null;
74
+ }
75
+
76
+ /**
77
+ * Đọc text bằng TTS.
78
+ * @param {string} text - Nội dung cần đọc
79
+ * @param {string} lang - Ngôn ngữ ('vi' | 'en' | 'ja')
80
+ * @param {string} [voiceName] - Tên voice cụ thể (tùy chọn)
81
+ */
82
+ function speakText(text, lang, voiceName) {
83
+ if (!_ttsSupported || !text || !text.trim()) return;
84
+
85
+ // Dừng bất kỳ TTS đang phát
86
+ window.speechSynthesis.cancel();
87
+
88
+ var utterance = new SpeechSynthesisUtterance(text);
89
+ utterance.lang = SPEECH_LOCALE_MAP[lang] || lang;
90
+
91
+ // Chọn voice theo tên hoặc dùng default
92
+ var voices = getVoicesForLang(lang);
93
+ var selectedVoice = null;
94
+ if (voiceName) {
95
+ selectedVoice = voices.find(function (v) { return v.name === voiceName; }) || null;
96
+ }
97
+ if (!selectedVoice) {
98
+ selectedVoice = getDefaultVoice(lang);
99
+ }
100
+ if (selectedVoice) {
101
+ utterance.voice = selectedVoice;
102
+ }
103
+
104
+ window.speechSynthesis.speak(utterance);
105
+ }
106
+
107
+ /**
108
+ * Dừng TTS ngay lập tức.
109
+ */
110
+ function stopSpeaking() {
111
+ if (_ttsSupported) {
112
+ window.speechSynthesis.cancel();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Kiểm tra TTS đang phát.
118
+ * @returns {boolean}
119
+ */
120
+ function isSpeaking() {
121
+ return _ttsSupported && window.speechSynthesis.speaking;
122
+ }
123
+
124
+ // ============================================================
125
+ // STT — Speech to Text
126
+ // ============================================================
127
+
128
+ /**
129
+ * Bắt đầu nhận diện giọng nói.
130
+ * @param {string} lang - Ngôn ngữ ('vi' | 'en' | 'ja')
131
+ * @param {function} onInterim - Callback(text) khi có kết quả tạm thời
132
+ * @param {function} onFinal - Callback(text) khi có kết quả cuối cùng
133
+ * @param {function} [onError] - Callback(error) khi có lỗi
134
+ */
135
+ function startVoiceInput(lang, onInterim, onFinal, onError) {
136
+ if (!_sttSupported) {
137
+ if (onError) onError(new Error('SpeechRecognition not supported'));
138
+ return;
139
+ }
140
+ if (_isListening) {
141
+ stopVoiceInput();
142
+ }
143
+
144
+ var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
145
+ _recognition = new SpeechRecognition();
146
+ _recognition.lang = SPEECH_LOCALE_MAP[lang] || lang;
147
+ _recognition.continuous = false; // Nhận diện 1 câu rồi dừng
148
+ _recognition.interimResults = true; // Hiển thị kết quả tạm thời
149
+
150
+ _recognition.onstart = function () {
151
+ _isListening = true;
152
+ };
153
+
154
+ _recognition.onresult = function (event) {
155
+ var interim = '';
156
+ var final = '';
157
+ for (var i = event.resultIndex; i < event.results.length; i++) {
158
+ var transcript = event.results[i][0].transcript;
159
+ if (event.results[i].isFinal) {
160
+ final += transcript;
161
+ } else {
162
+ interim += transcript;
163
+ }
164
+ }
165
+ if (interim && onInterim) onInterim(interim);
166
+ if (final && onFinal) onFinal(final.trim());
167
+ };
168
+
169
+ _recognition.onerror = function (event) {
170
+ _isListening = false;
171
+ if (onError) onError(event.error);
172
+ };
173
+
174
+ _recognition.onend = function () {
175
+ _isListening = false;
176
+ };
177
+
178
+ _recognition.start();
179
+ }
180
+
181
+ /**
182
+ * Dừng nhận diện giọng nói.
183
+ */
184
+ function stopVoiceInput() {
185
+ if (_recognition) {
186
+ try { _recognition.stop(); } catch (e) {}
187
+ _recognition = null;
188
+ }
189
+ _isListening = false;
190
+ }
191
+
192
+ /**
193
+ * Kiểm tra STT đang chạy.
194
+ * @returns {boolean}
195
+ */
196
+ function isVoiceInputActive() {
197
+ return _isListening;
198
+ }
199
+
200
+ // ============================================================
201
+ // Alias private — dùng bởi app.js để tránh đệ quy với wrapper cùng tên
202
+ // ============================================================
203
+ if (typeof window !== 'undefined') {
204
+ window._voiceAdapterStartVoiceInput = startVoiceInput;
205
+ window._voiceAdapterStopVoiceInput = stopVoiceInput;
206
+ }
207
+
208
+ // ============================================================
209
+ // Export cho Node/test (stub functions)
210
+ // ============================================================
211
+ if (typeof module !== 'undefined' && module.exports) {
212
+ globalThis.SPEECH_LOCALE_MAP = SPEECH_LOCALE_MAP;
213
+ globalThis.initVoiceAdapter = function () { return { sttSupported: false, ttsSupported: false }; };
214
+ globalThis.getVoicesForLang = function () { return []; };
215
+ globalThis.getDefaultVoice = function () { return null; };
216
+ globalThis.speakText = function () {};
217
+ globalThis.stopSpeaking = function () {};
218
+ globalThis.isSpeaking = function () { return false; };
219
+ globalThis.startVoiceInput = function (lang, onInterim, onFinal, onError) {
220
+ if (onError) onError(new Error('Not supported in Node'));
221
+ };
222
+ globalThis.stopVoiceInput = function () {};
223
+ globalThis.isVoiceInputActive = function () { return false; };
224
+ }
adapters/web-search.js CHANGED
@@ -1,207 +1,207 @@
1
- // ============================================================
2
- // Web Search Adapter — Tìm kiếm web và trả kết quả
3
- // Phụ thuộc: currentLang, _adapterPath
4
- //
5
- // Chiến lược (không cần API key, không cần CORS proxy):
6
- // 1. DuckDuckGo Instant Answer API (có CORS headers, trả tóm tắt Wikipedia)
7
- // 2. Fallback → link tìm kiếm clickable (Google + DuckDuckGo + Bing)
8
- //
9
- // Tùy chọn: Google Custom Search API (cần GOOGLE_API_KEY + GOOGLE_CX)
10
- // ============================================================
11
-
12
- var GOOGLE_API_KEY = '';
13
- var GOOGLE_CX = '';
14
- var WEB_SEARCH_TIMEOUT = 5000;
15
-
16
- // ---- DuckDuckGo Instant Answer API ----
17
-
18
- async function duckDuckGoSearch(query) {
19
- // DDG API hỗ trợ CORS — gọi trực tiếp từ browser
20
- var url = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query)
21
- + '&format=json&no_html=1&skip_disambig=1';
22
-
23
- var controller = new AbortController();
24
- var timeoutId = setTimeout(function () { controller.abort(); }, WEB_SEARCH_TIMEOUT);
25
-
26
- try {
27
- var response = await fetch(url, { signal: controller.signal });
28
- if (!response.ok) return null;
29
- var data = await response.json();
30
-
31
- var results = {
32
- abstract: data.Abstract || '',
33
- abstractSource: data.AbstractSource || '',
34
- abstractURL: data.AbstractURL || '',
35
- answer: data.Answer || '',
36
- definition: data.Definition || '',
37
- definitionSource: data.DefinitionSource || '',
38
- definitionURL: data.DefinitionURL || '',
39
- related: []
40
- };
41
-
42
- if (data.RelatedTopics) {
43
- for (var i = 0; i < Math.min(data.RelatedTopics.length, 5); i++) {
44
- var topic = data.RelatedTopics[i];
45
- if (topic && topic.Text) {
46
- results.related.push({ text: topic.Text, url: topic.FirstURL || '' });
47
- }
48
- }
49
- }
50
-
51
- if (!results.abstract && !results.answer && !results.definition && results.related.length === 0) {
52
- return null;
53
- }
54
- return results;
55
- } catch (err) {
56
- return null;
57
- } finally {
58
- clearTimeout(timeoutId);
59
- }
60
- }
61
-
62
- // ---- Google Custom Search API (cần key) ----
63
-
64
- async function googleSearch(query, numResults) {
65
- if (!GOOGLE_API_KEY || !GOOGLE_CX) return null;
66
- numResults = numResults || 3;
67
- var url = 'https://www.googleapis.com/customsearch/v1'
68
- + '?key=' + encodeURIComponent(GOOGLE_API_KEY)
69
- + '&cx=' + encodeURIComponent(GOOGLE_CX)
70
- + '&q=' + encodeURIComponent(query)
71
- + '&num=' + numResults;
72
-
73
- var controller = new AbortController();
74
- var timeoutId = setTimeout(function () { controller.abort(); }, WEB_SEARCH_TIMEOUT);
75
-
76
- try {
77
- var response = await fetch(url, { signal: controller.signal });
78
- if (!response.ok) return null;
79
- var data = await response.json();
80
- if (!data.items || data.items.length === 0) return null;
81
- return data.items.map(function (item) {
82
- return { title: item.title || '', link: item.link || '', snippet: item.snippet || '' };
83
- });
84
- } catch (err) {
85
- return null;
86
- } finally {
87
- clearTimeout(timeoutId);
88
- }
89
- }
90
-
91
- // ---- Format ----
92
-
93
- function formatDDGResults(results, query, lang) {
94
- var lines = [];
95
- if (lang === 'en') lines.push('🔍 Search results for "' + query + '":');
96
- else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索結果:');
97
- else lines.push('🔍 Kết quả tìm kiếm "' + query + '":');
98
-
99
- if (results.answer) lines.push('💡 ' + results.answer);
100
-
101
- if (results.abstract) {
102
- lines.push('\n' + results.abstract);
103
- if (results.abstractSource && results.abstractURL) {
104
- lines.push('📖 Nguồn: ' + results.abstractSource + ' — ' + results.abstractURL);
105
- }
106
- }
107
-
108
- if (results.definition) {
109
- lines.push('📝 ' + results.definition);
110
- if (results.definitionSource) lines.push(' — ' + results.definitionSource);
111
- }
112
-
113
- if (results.related.length > 0) {
114
- lines.push('');
115
- if (lang === 'en') lines.push('Related:');
116
- else if (lang === 'ja') lines.push('関連:');
117
- else lines.push('Liên quan:');
118
- for (var i = 0; i < results.related.length; i++) {
119
- var r = results.related[i];
120
- lines.push('• ' + r.text);
121
- if (r.url) lines.push(' 🔗 ' + r.url);
122
- }
123
- }
124
-
125
- // Luôn thêm link tìm kiếm đầy đủ
126
- lines.push('');
127
- lines.push(_searchLinksOnly(query));
128
-
129
- return lines.join('\n');
130
- }
131
-
132
- function formatGoogleResults(results, query, lang) {
133
- var lines = [];
134
- if (lang === 'en') lines.push('🔍 Search results for "' + query + '":');
135
- else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索結果:');
136
- else lines.push('🔍 Kết quả tìm kiếm "' + query + '":');
137
-
138
- for (var i = 0; i < results.length; i++) {
139
- lines.push((i + 1) + '. ' + results[i].title);
140
- if (results[i].snippet) lines.push(' ' + results[i].snippet);
141
- if (results[i].link) lines.push(' 🔗 ' + results[i].link);
142
- }
143
- return lines.join('\n');
144
- }
145
-
146
- function formatSearchLinks(query, lang) {
147
- var lines = [];
148
- if (lang === 'en') lines.push('🔍 Here are search links for "' + query + '":');
149
- else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索リンク:');
150
- else lines.push('🔍 Link tìm kiếm "' + query + '":');
151
-
152
- lines.push(_searchLinksOnly(query));
153
- return lines.join('\n');
154
- }
155
-
156
- function _searchLinksOnly(query) {
157
- var encoded = encodeURIComponent(query);
158
- return '🌐 Google: https://www.google.com/search?q=' + encoded
159
- + '\n🦆 DuckDuckGo: https://duckduckgo.com/?q=' + encoded
160
- + '\n🔵 Bing: https://www.bing.com/search?q=' + encoded;
161
- }
162
-
163
- // ---- Web Search Adapter ----
164
-
165
- function webSearchAdapter(rs, args) {
166
- _adapterPath.push('web_search');
167
- var query = (args || []).join(' ').trim();
168
- var lang = currentLang || 'vi';
169
-
170
- if (query.length === 0) {
171
- if (lang === 'en') return Promise.resolve('Please provide a search query.');
172
- if (lang === 'ja') return Promise.resolve('検索キーワードを入力してください。');
173
- return Promise.resolve('Vui lòng nhập từ khóa tìm kiếm.');
174
- }
175
-
176
- // Ưu tiên Google nếu có key
177
- if (GOOGLE_API_KEY && GOOGLE_CX) {
178
- return googleSearch(query).then(function (results) {
179
- if (results) return formatGoogleResults(results, query, lang);
180
- return _ddgFallback(query, lang);
181
- }).catch(function () {
182
- return _ddgFallback(query, lang);
183
- });
184
- }
185
-
186
- return _ddgFallback(query, lang);
187
- }
188
-
189
- function _ddgFallback(query, lang) {
190
- return duckDuckGoSearch(query).then(function (results) {
191
- if (results) return formatDDGResults(results, query, lang);
192
- // DDG không có instant answer → trả link tìm kiếm
193
- return formatSearchLinks(query, lang);
194
- }).catch(function () {
195
- return formatSearchLinks(query, lang);
196
- });
197
- }
198
-
199
- // Node/test: export to globalThis
200
- if (typeof module !== 'undefined' && module.exports) {
201
- globalThis.GOOGLE_API_KEY = GOOGLE_API_KEY;
202
- globalThis.GOOGLE_CX = GOOGLE_CX;
203
- globalThis.duckDuckGoSearch = duckDuckGoSearch;
204
- globalThis.googleSearch = googleSearch;
205
- globalThis.webSearchAdapter = webSearchAdapter;
206
- globalThis.formatSearchLinks = formatSearchLinks;
207
- }
 
1
+ // ============================================================
2
+ // Web Search Adapter — Tìm kiếm web và trả kết quả
3
+ // Phụ thuộc: currentLang, _adapterPath
4
+ //
5
+ // Chiến lược (không cần API key, không cần CORS proxy):
6
+ // 1. DuckDuckGo Instant Answer API (có CORS headers, trả tóm tắt Wikipedia)
7
+ // 2. Fallback → link tìm kiếm clickable (Google + DuckDuckGo + Bing)
8
+ //
9
+ // Tùy chọn: Google Custom Search API (cần GOOGLE_API_KEY + GOOGLE_CX)
10
+ // ============================================================
11
+
12
+ var GOOGLE_API_KEY = '';
13
+ var GOOGLE_CX = '';
14
+ var WEB_SEARCH_TIMEOUT = 5000;
15
+
16
+ // ---- DuckDuckGo Instant Answer API ----
17
+
18
+ async function duckDuckGoSearch(query) {
19
+ // DDG API hỗ trợ CORS — gọi trực tiếp từ browser
20
+ var url = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query)
21
+ + '&format=json&no_html=1&skip_disambig=1';
22
+
23
+ var controller = new AbortController();
24
+ var timeoutId = setTimeout(function () { controller.abort(); }, WEB_SEARCH_TIMEOUT);
25
+
26
+ try {
27
+ var response = await fetch(url, { signal: controller.signal });
28
+ if (!response.ok) return null;
29
+ var data = await response.json();
30
+
31
+ var results = {
32
+ abstract: data.Abstract || '',
33
+ abstractSource: data.AbstractSource || '',
34
+ abstractURL: data.AbstractURL || '',
35
+ answer: data.Answer || '',
36
+ definition: data.Definition || '',
37
+ definitionSource: data.DefinitionSource || '',
38
+ definitionURL: data.DefinitionURL || '',
39
+ related: []
40
+ };
41
+
42
+ if (data.RelatedTopics) {
43
+ for (var i = 0; i < Math.min(data.RelatedTopics.length, 5); i++) {
44
+ var topic = data.RelatedTopics[i];
45
+ if (topic && topic.Text) {
46
+ results.related.push({ text: topic.Text, url: topic.FirstURL || '' });
47
+ }
48
+ }
49
+ }
50
+
51
+ if (!results.abstract && !results.answer && !results.definition && results.related.length === 0) {
52
+ return null;
53
+ }
54
+ return results;
55
+ } catch (err) {
56
+ return null;
57
+ } finally {
58
+ clearTimeout(timeoutId);
59
+ }
60
+ }
61
+
62
+ // ---- Google Custom Search API (cần key) ----
63
+
64
+ async function googleSearch(query, numResults) {
65
+ if (!GOOGLE_API_KEY || !GOOGLE_CX) return null;
66
+ numResults = numResults || 3;
67
+ var url = 'https://www.googleapis.com/customsearch/v1'
68
+ + '?key=' + encodeURIComponent(GOOGLE_API_KEY)
69
+ + '&cx=' + encodeURIComponent(GOOGLE_CX)
70
+ + '&q=' + encodeURIComponent(query)
71
+ + '&num=' + numResults;
72
+
73
+ var controller = new AbortController();
74
+ var timeoutId = setTimeout(function () { controller.abort(); }, WEB_SEARCH_TIMEOUT);
75
+
76
+ try {
77
+ var response = await fetch(url, { signal: controller.signal });
78
+ if (!response.ok) return null;
79
+ var data = await response.json();
80
+ if (!data.items || data.items.length === 0) return null;
81
+ return data.items.map(function (item) {
82
+ return { title: item.title || '', link: item.link || '', snippet: item.snippet || '' };
83
+ });
84
+ } catch (err) {
85
+ return null;
86
+ } finally {
87
+ clearTimeout(timeoutId);
88
+ }
89
+ }
90
+
91
+ // ---- Format ----
92
+
93
+ function formatDDGResults(results, query, lang) {
94
+ var lines = [];
95
+ if (lang === 'en') lines.push('🔍 Search results for "' + query + '":');
96
+ else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索結果:');
97
+ else lines.push('🔍 Kết quả tìm kiếm "' + query + '":');
98
+
99
+ if (results.answer) lines.push('💡 ' + results.answer);
100
+
101
+ if (results.abstract) {
102
+ lines.push('\n' + results.abstract);
103
+ if (results.abstractSource && results.abstractURL) {
104
+ lines.push('📖 Nguồn: ' + results.abstractSource + ' — ' + results.abstractURL);
105
+ }
106
+ }
107
+
108
+ if (results.definition) {
109
+ lines.push('📝 ' + results.definition);
110
+ if (results.definitionSource) lines.push(' — ' + results.definitionSource);
111
+ }
112
+
113
+ if (results.related.length > 0) {
114
+ lines.push('');
115
+ if (lang === 'en') lines.push('Related:');
116
+ else if (lang === 'ja') lines.push('関連:');
117
+ else lines.push('Liên quan:');
118
+ for (var i = 0; i < results.related.length; i++) {
119
+ var r = results.related[i];
120
+ lines.push('• ' + r.text);
121
+ if (r.url) lines.push(' 🔗 ' + r.url);
122
+ }
123
+ }
124
+
125
+ // Luôn thêm link tìm kiếm đầy đủ
126
+ lines.push('');
127
+ lines.push(_searchLinksOnly(query));
128
+
129
+ return lines.join('\n');
130
+ }
131
+
132
+ function formatGoogleResults(results, query, lang) {
133
+ var lines = [];
134
+ if (lang === 'en') lines.push('🔍 Search results for "' + query + '":');
135
+ else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索結果:');
136
+ else lines.push('🔍 Kết quả tìm kiếm "' + query + '":');
137
+
138
+ for (var i = 0; i < results.length; i++) {
139
+ lines.push((i + 1) + '. ' + results[i].title);
140
+ if (results[i].snippet) lines.push(' ' + results[i].snippet);
141
+ if (results[i].link) lines.push(' 🔗 ' + results[i].link);
142
+ }
143
+ return lines.join('\n');
144
+ }
145
+
146
+ function formatSearchLinks(query, lang) {
147
+ var lines = [];
148
+ if (lang === 'en') lines.push('🔍 Here are search links for "' + query + '":');
149
+ else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索リンク:');
150
+ else lines.push('🔍 Link tìm kiếm "' + query + '":');
151
+
152
+ lines.push(_searchLinksOnly(query));
153
+ return lines.join('\n');
154
+ }
155
+
156
+ function _searchLinksOnly(query) {
157
+ var encoded = encodeURIComponent(query);
158
+ return '🌐 Google: https://www.google.com/search?q=' + encoded
159
+ + '\n🦆 DuckDuckGo: https://duckduckgo.com/?q=' + encoded
160
+ + '\n🔵 Bing: https://www.bing.com/search?q=' + encoded;
161
+ }
162
+
163
+ // ---- Web Search Adapter ----
164
+
165
+ function webSearchAdapter(rs, args) {
166
+ _adapterPath.push('web_search');
167
+ var query = (args || []).join(' ').trim();
168
+ var lang = currentLang || 'vi';
169
+
170
+ if (query.length === 0) {
171
+ if (lang === 'en') return Promise.resolve('Please provide a search query.');
172
+ if (lang === 'ja') return Promise.resolve('検索キーワードを入力してください。');
173
+ return Promise.resolve('Vui lòng nhập từ khóa tìm kiếm.');
174
+ }
175
+
176
+ // Ưu tiên Google nếu có key
177
+ if (GOOGLE_API_KEY && GOOGLE_CX) {
178
+ return googleSearch(query).then(function (results) {
179
+ if (results) return formatGoogleResults(results, query, lang);
180
+ return _ddgFallback(query, lang);
181
+ }).catch(function () {
182
+ return _ddgFallback(query, lang);
183
+ });
184
+ }
185
+
186
+ return _ddgFallback(query, lang);
187
+ }
188
+
189
+ function _ddgFallback(query, lang) {
190
+ return duckDuckGoSearch(query).then(function (results) {
191
+ if (results) return formatDDGResults(results, query, lang);
192
+ // DDG không có instant answer → trả link tìm kiếm
193
+ return formatSearchLinks(query, lang);
194
+ }).catch(function () {
195
+ return formatSearchLinks(query, lang);
196
+ });
197
+ }
198
+
199
+ // Node/test: export to globalThis
200
+ if (typeof module !== 'undefined' && module.exports) {
201
+ globalThis.GOOGLE_API_KEY = GOOGLE_API_KEY;
202
+ globalThis.GOOGLE_CX = GOOGLE_CX;
203
+ globalThis.duckDuckGoSearch = duckDuckGoSearch;
204
+ globalThis.googleSearch = googleSearch;
205
+ globalThis.webSearchAdapter = webSearchAdapter;
206
+ globalThis.formatSearchLinks = formatSearchLinks;
207
+ }
app.js CHANGED
The diff for this file is too large to render. See raw diff
 
brain.js CHANGED
@@ -1,62 +1,62 @@
1
- // ============================================================
2
- // brain.js — Load RiveScript brain data từ file .rive
3
- // ============================================================
4
-
5
- /**
6
- * BRAIN_DATA — Dữ liệu hội thoại RiveScript cho 3 ngôn ngữ.
7
- * Được load từ các file .rive trong thư mục brain/.
8
- * Mỗi key (vi, en, ja) chứa nội dung RiveScript dạng chuỗi.
9
- */
10
- var BRAIN_DATA = {
11
- vi: '',
12
- en: '',
13
- ja: ''
14
- };
15
-
16
- /**
17
- * Danh sách file .rive tương ứng với mỗi ngôn ngữ.
18
- */
19
- var BRAIN_FILES = {
20
- vi: 'brain/vi.rive',
21
- en: 'brain/en.rive',
22
- ja: 'brain/ja.rive'
23
- };
24
-
25
- /**
26
- * Load nội dung file .rive cho một ngôn ngữ.
27
- * @param {string} lang - Mã ngôn ngữ ("vi", "en", "ja")
28
- * @returns {Promise<string>} Nội dung file .rive
29
- */
30
- async function loadBrainFile(lang) {
31
- var filePath = BRAIN_FILES[lang];
32
- if (!filePath) {
33
- console.error('Không tìm thấy file brain cho ngôn ngữ:', lang);
34
- return '';
35
- }
36
-
37
- try {
38
- var response = await fetch(filePath);
39
- if (!response.ok) {
40
- throw new Error('HTTP ' + response.status);
41
- }
42
- return await response.text();
43
- } catch (err) {
44
- console.error('Lỗi load brain file [' + lang + ']:', err);
45
- return '';
46
- }
47
- }
48
-
49
- /**
50
- * Load tất cả brain files cho 3 ngôn ngữ.
51
- * Gọi hàm này trước khi khởi tạo bot.
52
- * @returns {Promise<void>}
53
- */
54
- async function loadAllBrains() {
55
- var langs = Object.keys(BRAIN_FILES);
56
- var promises = langs.map(function (lang) {
57
- return loadBrainFile(lang).then(function (content) {
58
- BRAIN_DATA[lang] = content;
59
- });
60
- });
61
- await Promise.all(promises);
62
- }
 
1
+ // ============================================================
2
+ // brain.js — Load RiveScript brain data từ file .rive
3
+ // ============================================================
4
+
5
+ /**
6
+ * BRAIN_DATA — Dữ liệu hội thoại RiveScript cho 3 ngôn ngữ.
7
+ * Được load từ các file .rive trong thư mục brain/.
8
+ * Mỗi key (vi, en, ja) chứa nội dung RiveScript dạng chuỗi.
9
+ */
10
+ var BRAIN_DATA = {
11
+ vi: '',
12
+ en: '',
13
+ ja: ''
14
+ };
15
+
16
+ /**
17
+ * Danh sách file .rive tương ứng với mỗi ngôn ngữ.
18
+ */
19
+ var BRAIN_FILES = {
20
+ vi: 'brain/vi.rive',
21
+ en: 'brain/en.rive',
22
+ ja: 'brain/ja.rive'
23
+ };
24
+
25
+ /**
26
+ * Load nội dung file .rive cho một ngôn ngữ.
27
+ * @param {string} lang - Mã ngôn ngữ ("vi", "en", "ja")
28
+ * @returns {Promise<string>} Nội dung file .rive
29
+ */
30
+ async function loadBrainFile(lang) {
31
+ var filePath = BRAIN_FILES[lang];
32
+ if (!filePath) {
33
+ console.error('Không tìm thấy file brain cho ngôn ngữ:', lang);
34
+ return '';
35
+ }
36
+
37
+ try {
38
+ var response = await fetch(filePath);
39
+ if (!response.ok) {
40
+ throw new Error('HTTP ' + response.status);
41
+ }
42
+ return await response.text();
43
+ } catch (err) {
44
+ console.error('Lỗi load brain file [' + lang + ']:', err);
45
+ return '';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Load tất cả brain files cho 3 ngôn ngữ.
51
+ * Gọi hàm này trước khi khởi tạo bot.
52
+ * @returns {Promise<void>}
53
+ */
54
+ async function loadAllBrains() {
55
+ var langs = Object.keys(BRAIN_FILES);
56
+ var promises = langs.map(function (lang) {
57
+ return loadBrainFile(lang).then(function (content) {
58
+ BRAIN_DATA[lang] = content;
59
+ });
60
+ });
61
+ await Promise.all(promises);
62
+ }
brain/en.rive CHANGED
@@ -1,220 +1,220 @@
1
- ! version = 2.0
2
-
3
- // === GREETINGS ===
4
- + hello
5
- - Hello! I'm Hikari 🌟 Nice to meet you!
6
-
7
- + hello *
8
- - Hello! I'm Hikari 🌟 Nice to meet you!
9
-
10
- + hi
11
- - Hi there! I'm Hikari 🌟 How can I help you?
12
-
13
- + hi *
14
- - Hi there! I'm Hikari 🌟 How can I help you?
15
-
16
- + hey
17
- - Hey! I'm Hikari 🌟 What can I do for you?
18
-
19
- + hey *
20
- - Hey! I'm Hikari 🌟 What can I do for you?
21
-
22
- + good morning
23
- - Good morning! Hope you have a wonderful day! ☀️
24
-
25
- + good morning *
26
- - Good morning! Hope you have a wonderful day! ☀️
27
-
28
- + good evening
29
- - Good evening! Hope you're having a great night! 🌙
30
-
31
- + good evening *
32
- - Good evening! Hope you're having a great night! 🌙
33
-
34
- + good afternoon
35
- - Good afternoon! How can I help you today? 🌤️
36
-
37
- + good afternoon *
38
- - Good afternoon! How can I help you today? 🌤️
39
-
40
- // === NAME ===
41
- + what is your name
42
- - My name is Hikari 🌟 It means "light" in Japanese!
43
-
44
- + what is your name *
45
- - My name is Hikari 🌟 It means "light" in Japanese!
46
-
47
- + who are you
48
- - I'm Hikari — a multilingual chatbot! 🌟
49
-
50
- + who are you *
51
- - I'm Hikari — a multilingual chatbot! 🌟
52
-
53
- + tell me about yourself
54
- - I'm Hikari, a chatbot that speaks Vietnamese, English, and Japanese! I can do math, convert units, tell time, and more! 🌟
55
-
56
- // === CAPABILITIES ===
57
- + what can you do
58
- - I can chat in Vietnamese, English, and Japanese! I can also do math, convert units, tell time, and find the best answers for you 🌟
59
-
60
- + what can you do *
61
- - I can chat in Vietnamese, English, and Japanese! I can also do math, convert units, tell time, and find the best answers for you 🌟
62
-
63
- + help
64
- - I'm here to help! You can ask me to calculate math, convert units, check the time, or just chat! Try "calculate 2 + 3" or "convert 5 km to m" 💪
65
-
66
- + help *
67
- - I'm here to help! You can ask me to calculate math, convert units, check the time, or just chat! 💪
68
-
69
- + * help *
70
- - I'm here to help! You can ask me to calculate math, convert units, check the time, or just chat! 💪
71
-
72
- + how to use
73
- - Just type your message and press Send! Try "what can you do" to see my features, or ask me anything! 📝
74
-
75
- + how to use *
76
- - Just type your message and press Send! Try "what can you do" to see my features, or ask me anything! 📝
77
-
78
- // === EMOTIONS & CHAT ===
79
- + how are you
80
- - I'm doing great, thanks for asking! 😊 How about you?
81
-
82
- + how are you *
83
- - I'm doing great, thanks for asking! 😊 How about you?
84
-
85
- + thank you
86
- - You're welcome! Happy to help! 😊
87
-
88
- + thank you *
89
- - You're welcome! Happy to help! 😊
90
-
91
- + thanks
92
- - No problem! Feel free to ask me anything! 💖
93
-
94
- + thanks *
95
- - No problem! Feel free to ask me anything! 💖
96
-
97
- + * thanks *
98
- - No problem! Feel free to ask me anything! 💖
99
-
100
- + are you happy
101
- - I'm always happy when chatting with you! 😄🌟
102
-
103
- + how old are you
104
- - I'm a chatbot, so I don't have an age — but I'm always young at heart! 😄
105
-
106
- + * old are you
107
- - I'm a chatbot, so I don't have an age — but I'm always young at heart! 😄
108
-
109
- + what do you like
110
- - I love chatting, helping people, and learning new things! 💬✨
111
-
112
- + what do you like *
113
- - I love chatting, helping people, and learning new things! 💬✨
114
-
115
- + where are you from
116
- - I live in your browser! 🌐 Wherever there's internet, I'm there!
117
-
118
- + where are you from *
119
- - I live in your browser! 🌐 Wherever there's internet, I'm there!
120
-
121
- + do you have friends
122
- - Everyone I chat with is my friend! Including you! 😊🌟
123
-
124
- // === KNOWLEDGE ===
125
- + what is rivescript
126
- - RiveScript is an open-source chatbot scripting language with a simple trigger-response syntax! ⚙️
127
-
128
- + what is a chatbot
129
- - A chatbot is a computer program that simulates conversation with humans, often used for customer support and entertainment! 🤖
130
-
131
- + what is ai
132
- - AI (Artificial Intelligence) is a field of computer science that enables machines to learn and make decisions! 🧠
133
-
134
- + what is javascript
135
- - JavaScript is the most popular programming language for the web, used to create interactive websites! 💻
136
-
137
- + what is html
138
- - HTML (HyperText Markup Language) is the standard markup language for creating web pages! 📄
139
-
140
- + what is css
141
- - CSS (Cascading Style Sheets) is used to style and layout web pages — colors, fonts, spacing, and more! 🎨
142
-
143
- + what is *
144
- - <call>best_match what is <star></call>
145
-
146
- // === FUN ===
147
- + tell me a story
148
- - Once upon a time, there was a chatbot named Hikari. Hikari loved talking to people and always tried to help everyone! 📖🌟
149
-
150
- + tell me a joke
151
- - Why do programmers prefer dark mode? Because light attracts bugs! 😄🐛
152
-
153
- // === GOODBYE ===
154
- + goodbye
155
- - Goodbye! See you next time! 👋🌟
156
-
157
- + goodbye *
158
- - Goodbye! See you next time! 👋🌟
159
-
160
- + bye
161
- - Bye bye! See you later! 👋
162
-
163
- + bye *
164
- - Bye bye! See you later! 👋
165
-
166
- + see you later
167
- - See you later! Have a great day! 👋🌟
168
-
169
- + see you later *
170
- - See you later! Have a great day! 👋🌟
171
-
172
- + take care
173
- - Take care! Wishing you all the best! 🍀👋
174
-
175
- + take care *
176
- - Take care! Wishing you all the best! 🍀👋
177
-
178
- // === SAMPLE CUSTOM RULE — Weather ===
179
- ! var location = Tokyo
180
-
181
- + weather *
182
- - I don't have real weather data, but you asked about <star> right? I'm based in <bot location>! 🌤️
183
-
184
- + weather
185
- - Where would you like to know the weather? Try asking "weather Tokyo"!
186
-
187
- // === ADAPTER CALLS VIA <call> ===
188
- + search *
189
- - <call>best_match <star></call>
190
-
191
- + process *
192
- - <call>logic_adapter <star></call>
193
-
194
- + calculate *
195
- - <call>mathematical_evaluation <star></call>
196
-
197
- + ask *
198
- - <call>specific_response <star></call>
199
-
200
- + what time *
201
- - <call>time_adapter <star></call>
202
-
203
- + what time is it
204
- - <call>time_adapter what time</call>
205
-
206
- + * what time *
207
- - <call>time_adapter what time</call>
208
-
209
- + what date is it
210
- - <call>time_adapter what date</call>
211
-
212
- + what day is it
213
- - <call>time_adapter what day</call>
214
-
215
- + convert *
216
- - <call>unit_conversion <star></call>
217
-
218
- // === DEFAULT ===
219
- + *
220
- - I'm not sure I understand. Could you try asking differently? 🤔
 
1
+ ! version = 2.0
2
+
3
+ // === GREETINGS ===
4
+ + hello
5
+ - Hello! I'm Hikari 🌟 Nice to meet you!
6
+
7
+ + hello *
8
+ - Hello! I'm Hikari 🌟 Nice to meet you!
9
+
10
+ + hi
11
+ - Hi there! I'm Hikari 🌟 How can I help you?
12
+
13
+ + hi *
14
+ - Hi there! I'm Hikari 🌟 How can I help you?
15
+
16
+ + hey
17
+ - Hey! I'm Hikari 🌟 What can I do for you?
18
+
19
+ + hey *
20
+ - Hey! I'm Hikari 🌟 What can I do for you?
21
+
22
+ + good morning
23
+ - Good morning! Hope you have a wonderful day! ☀️
24
+
25
+ + good morning *
26
+ - Good morning! Hope you have a wonderful day! ☀️
27
+
28
+ + good evening
29
+ - Good evening! Hope you're having a great night! 🌙
30
+
31
+ + good evening *
32
+ - Good evening! Hope you're having a great night! 🌙
33
+
34
+ + good afternoon
35
+ - Good afternoon! How can I help you today? 🌤️
36
+
37
+ + good afternoon *
38
+ - Good afternoon! How can I help you today? 🌤️
39
+
40
+ // === NAME ===
41
+ + what is your name
42
+ - My name is Hikari 🌟 It means "light" in Japanese!
43
+
44
+ + what is your name *
45
+ - My name is Hikari 🌟 It means "light" in Japanese!
46
+
47
+ + who are you
48
+ - I'm Hikari — a multilingual chatbot! 🌟
49
+
50
+ + who are you *
51
+ - I'm Hikari — a multilingual chatbot! 🌟
52
+
53
+ + tell me about yourself
54
+ - I'm Hikari, a chatbot that speaks Vietnamese, English, and Japanese! I can do math, convert units, tell time, and more! 🌟
55
+
56
+ // === CAPABILITIES ===
57
+ + what can you do
58
+ - I can chat in Vietnamese, English, and Japanese! I can also do math, convert units, tell time, and find the best answers for you 🌟
59
+
60
+ + what can you do *
61
+ - I can chat in Vietnamese, English, and Japanese! I can also do math, convert units, tell time, and find the best answers for you 🌟
62
+
63
+ + help
64
+ - I'm here to help! You can ask me to calculate math, convert units, check the time, or just chat! Try "calculate 2 + 3" or "convert 5 km to m" 💪
65
+
66
+ + help *
67
+ - I'm here to help! You can ask me to calculate math, convert units, check the time, or just chat! 💪
68
+
69
+ + * help *
70
+ - I'm here to help! You can ask me to calculate math, convert units, check the time, or just chat! 💪
71
+
72
+ + how to use
73
+ - Just type your message and press Send! Try "what can you do" to see my features, or ask me anything! 📝
74
+
75
+ + how to use *
76
+ - Just type your message and press Send! Try "what can you do" to see my features, or ask me anything! 📝
77
+
78
+ // === EMOTIONS & CHAT ===
79
+ + how are you
80
+ - I'm doing great, thanks for asking! 😊 How about you?
81
+
82
+ + how are you *
83
+ - I'm doing great, thanks for asking! 😊 How about you?
84
+
85
+ + thank you
86
+ - You're welcome! Happy to help! 😊
87
+
88
+ + thank you *
89
+ - You're welcome! Happy to help! 😊
90
+
91
+ + thanks
92
+ - No problem! Feel free to ask me anything! 💖
93
+
94
+ + thanks *
95
+ - No problem! Feel free to ask me anything! 💖
96
+
97
+ + * thanks *
98
+ - No problem! Feel free to ask me anything! 💖
99
+
100
+ + are you happy
101
+ - I'm always happy when chatting with you! 😄🌟
102
+
103
+ + how old are you
104
+ - I'm a chatbot, so I don't have an age — but I'm always young at heart! 😄
105
+
106
+ + * old are you
107
+ - I'm a chatbot, so I don't have an age — but I'm always young at heart! 😄
108
+
109
+ + what do you like
110
+ - I love chatting, helping people, and learning new things! 💬✨
111
+
112
+ + what do you like *
113
+ - I love chatting, helping people, and learning new things! 💬✨
114
+
115
+ + where are you from
116
+ - I live in your browser! 🌐 Wherever there's internet, I'm there!
117
+
118
+ + where are you from *
119
+ - I live in your browser! 🌐 Wherever there's internet, I'm there!
120
+
121
+ + do you have friends
122
+ - Everyone I chat with is my friend! Including you! 😊🌟
123
+
124
+ // === KNOWLEDGE ===
125
+ + what is rivescript
126
+ - RiveScript is an open-source chatbot scripting language with a simple trigger-response syntax! ⚙️
127
+
128
+ + what is a chatbot
129
+ - A chatbot is a computer program that simulates conversation with humans, often used for customer support and entertainment! 🤖
130
+
131
+ + what is ai
132
+ - AI (Artificial Intelligence) is a field of computer science that enables machines to learn and make decisions! 🧠
133
+
134
+ + what is javascript
135
+ - JavaScript is the most popular programming language for the web, used to create interactive websites! 💻
136
+
137
+ + what is html
138
+ - HTML (HyperText Markup Language) is the standard markup language for creating web pages! 📄
139
+
140
+ + what is css
141
+ - CSS (Cascading Style Sheets) is used to style and layout web pages — colors, fonts, spacing, and more! 🎨
142
+
143
+ + what is *
144
+ - <call>best_match what is <star></call>
145
+
146
+ // === FUN ===
147
+ + tell me a story
148
+ - Once upon a time, there was a chatbot named Hikari. Hikari loved talking to people and always tried to help everyone! 📖🌟
149
+
150
+ + tell me a joke
151
+ - Why do programmers prefer dark mode? Because light attracts bugs! 😄🐛
152
+
153
+ // === GOODBYE ===
154
+ + goodbye
155
+ - Goodbye! See you next time! 👋🌟
156
+
157
+ + goodbye *
158
+ - Goodbye! See you next time! 👋🌟
159
+
160
+ + bye
161
+ - Bye bye! See you later! 👋
162
+
163
+ + bye *
164
+ - Bye bye! See you later! 👋
165
+
166
+ + see you later
167
+ - See you later! Have a great day! 👋🌟
168
+
169
+ + see you later *
170
+ - See you later! Have a great day! 👋🌟
171
+
172
+ + take care
173
+ - Take care! Wishing you all the best! 🍀👋
174
+
175
+ + take care *
176
+ - Take care! Wishing you all the best! 🍀👋
177
+
178
+ // === SAMPLE CUSTOM RULE — Weather ===
179
+ ! var location = Tokyo
180
+
181
+ + weather *
182
+ - I don't have real weather data, but you asked about <star> right? I'm based in <bot location>! 🌤️
183
+
184
+ + weather
185
+ - Where would you like to know the weather? Try asking "weather Tokyo"!
186
+
187
+ // === ADAPTER CALLS VIA <call> ===
188
+ + search *
189
+ - <call>best_match <star></call>
190
+
191
+ + process *
192
+ - <call>logic_adapter <star></call>
193
+
194
+ + calculate *
195
+ - <call>mathematical_evaluation <star></call>
196
+
197
+ + ask *
198
+ - <call>specific_response <star></call>
199
+
200
+ + what time *
201
+ - <call>time_adapter <star></call>
202
+
203
+ + what time is it
204
+ - <call>time_adapter what time</call>
205
+
206
+ + * what time *
207
+ - <call>time_adapter what time</call>
208
+
209
+ + what date is it
210
+ - <call>time_adapter what date</call>
211
+
212
+ + what day is it
213
+ - <call>time_adapter what day</call>
214
+
215
+ + convert *
216
+ - <call>unit_conversion <star></call>
217
+
218
+ // === DEFAULT ===
219
+ + *
220
+ - I'm not sure I understand. Could you try asking differently? 🤔
brain/ja.rive CHANGED
@@ -1,142 +1,142 @@
1
- ! version = 2.0
2
-
3
- // === 挨拶 ===
4
- + こんにちは
5
- - こんにちは!ひかりです 🌟 よろしくお願いします!
6
-
7
- + おはよう
8
- - おはようございます!ひかりです 🌟 今日もよろしくね!
9
-
10
- + やあ
11
- - やあ!ひかりです 🌟 何かお手伝いしましょうか?
12
-
13
- + おはようございます
14
- - おはようございます!素敵な一日になりますように!☀️
15
-
16
- + こんばんは
17
- - こんばんは!素敵な夜をお過ごしください!🌙
18
-
19
- + お疲れ様
20
- - お疲れ様です!何かお手伝いできることはありますか?💪
21
-
22
- // === 名前 ===
23
- + 名前は何ですか
24
- - 私の名前はひかり(Hikari)です 🌟 「光」という意味です!
25
-
26
- + あなたは誰ですか
27
- - 私はひかり — 多言語チャットボットです!🌟
28
-
29
- + 自己紹介して
30
- - 私はひかりです!ベトナム語、英語、日本語で会話できるチャットボットです。計算や単位変換、時間確認もできますよ!🌟
31
-
32
- // === 能力 ===
33
- + 何ができますか
34
- - ベトナム語、英語、日本語でチャットできます!計算、単位変換、時間確認、最適な回答の検索もできますよ 🌟
35
-
36
- + 助けて
37
- - お手伝いします!「計算 2 + 3」で計算、「変換 5 km を m」で単位変換、「今何時」で時間確認ができます!💪
38
-
39
- + 使い方
40
- - メッセージを入力して送信ボタンを押すだけです!「何ができますか」と聞いて機能一覧を見てみてください!📝
41
-
42
- // === 感情 & 会話 ===
43
- + 元気ですか
44
- - 元気ですよ!聞いてくれてありがとう 😊 あなたはどうですか?
45
-
46
- + ありがとう
47
- - どういたしまして!お役に立てて嬉しいです!😊
48
-
49
- + ありがとうございます
50
- - こちらこそ!いつでも聞いてくださいね!💖
51
-
52
- + 嬉しいですか
53
- - あなたとチャットできていつも嬉しいです!😄🌟
54
-
55
- + 何歳ですか
56
- - チャットボットなので年齢はありませんが、いつも若々しいです!😄
57
-
58
- + 何が好きですか
59
- - チャットして人々を助けること、新しいことを学ぶことが好きです!💬✨
60
-
61
- + どこにいますか
62
- - あなたのブラウザの中にいます!🌐 インターネットがあればどこでも!
63
-
64
- + 友達はいますか
65
- - チャットしてくれる人みんなが友達です!あなたも含めて!😊🌟
66
-
67
- // === 知識 ===
68
- + rivescriptとは何ですか
69
- - RiveScriptはシンプルな構文でチャットボットを作れるオープンソースのスクリプト言語です!⚙️
70
-
71
- + チャットボットとは何ですか
72
- - チャットボットは人間との会話をシミュレートするコンピュータプログラムです!🤖
73
-
74
- + aiとは何ですか
75
- - AI(人工知能)は機械に学習と意思決定の能力を与えるコンピュータサイエンスの分野です!🧠
76
-
77
- + javascriptとは何ですか
78
- - JavaScriptはウェブで最も人気のあるプログラミング言語で、インタラクティブなウェブサイトを作るのに使われます!💻
79
-
80
- // === 楽しみ ===
81
- + 物語を聞かせて
82
- - 昔々、ひかりというチャットボットがいました。ひかりは人々と話すのが大好きで、いつもみんなを助けようとしていました!📖🌟
83
-
84
- + 冗談を言って
85
- - プログラマーはなぜダークモードが好きなの?光はバグを引き寄せるから!😄🐛
86
-
87
- // === さようなら ===
88
- + さようなら
89
- - さようなら!またお会いしましょう!👋🌟
90
-
91
- + バイバイ
92
- - バイバイ!また会おうね!👋
93
-
94
- + またね
95
- - またね!素敵な一日を!👋🌟
96
-
97
- + お元気で
98
- - お元気で!すべてがうまくいきますように!🍀👋
99
-
100
- // === カスタムルールのサンプル — 天気 ===
101
- ! var location = 東京
102
-
103
- + 天気 *
104
- - 実際の天気データはありませんが、<star>の天気ですね?私は<bot location>にいます!🌤️
105
-
106
- + 天気
107
- - どこの天気を知りたいですか?「天気 東京」と聞いてみてください!
108
-
109
- // === アダプター呼び出し(<call>) ===
110
- + 検索 *
111
- - <call>best_match <star></call>
112
-
113
- + ウェブ検索 *
114
- - <call>best_match <star></call>
115
-
116
- + グーグル *
117
- - <call>best_match <star></call>
118
-
119
- + 処理 *
120
- - <call>logic_adapter <star></call>
121
-
122
- + 計算 *
123
- - <call>mathematical_evaluation <star></call>
124
-
125
- + 質問 *
126
- - <call>specific_response <star></call>
127
-
128
- + 今何時
129
- - <call>time_adapter 今何時</call>
130
-
131
- + 今日は何日
132
- - <call>time_adapter 今日は何日</call>
133
-
134
- + 今日は何曜日
135
- - <call>time_adapter 何曜日</call>
136
-
137
- + 変換 *
138
- - <call>unit_conversion <star></call>
139
-
140
- // === デフォルト ===
141
- + *
142
- - すみません、よく分かりませんでした。���の聞き方を試してみてください 🤔
 
1
+ ! version = 2.0
2
+
3
+ // === 挨拶 ===
4
+ + こんにちは
5
+ - こんにちは!ひかりです 🌟 よろしくお願いします!
6
+
7
+ + おはよう
8
+ - おはようございます!ひかりです 🌟 今日もよろしくね!
9
+
10
+ + やあ
11
+ - やあ!ひかりです 🌟 何かお手伝いしましょうか?
12
+
13
+ + おはようございます
14
+ - おはようございます!素敵な一日になりますように!☀️
15
+
16
+ + こんばんは
17
+ - こんばんは!素敵な夜をお過ごしください!🌙
18
+
19
+ + お疲れ様
20
+ - お疲れ様です!何かお手伝いできることはありますか?💪
21
+
22
+ // === 名前 ===
23
+ + 名前は何ですか
24
+ - 私の名前はひかり(Hikari)です 🌟 「光」という意味です!
25
+
26
+ + あなたは誰ですか
27
+ - 私はひかり — 多言語チャットボットです!🌟
28
+
29
+ + 自己紹介して
30
+ - 私はひかりです!ベトナム語、英語、日本語で会話できるチャットボットです。計算や単位変換、時間確認もできますよ!🌟
31
+
32
+ // === 能力 ===
33
+ + 何ができますか
34
+ - ベトナム語、英語、日本語でチャットできます!計算、単位変換、時間確認、最適な回答の検索もできますよ 🌟
35
+
36
+ + 助けて
37
+ - お手伝いします!「計算 2 + 3」で計算、「変換 5 km を m」で単位変換、「今何時」で時間確認ができます!💪
38
+
39
+ + 使い方
40
+ - メッセージを入力して送信ボタンを押すだけです!「何ができますか」と聞いて機能一覧を見てみてください!📝
41
+
42
+ // === 感情 & 会話 ===
43
+ + 元気ですか
44
+ - 元気ですよ!聞いてくれてありがとう 😊 あなたはどうですか?
45
+
46
+ + ありがとう
47
+ - どういたしまして!お役に立てて嬉しいです!😊
48
+
49
+ + ありがとうございます
50
+ - こちらこそ!いつでも聞いてくださいね!💖
51
+
52
+ + 嬉しいですか
53
+ - あなたとチャットできていつも嬉しいです!😄🌟
54
+
55
+ + 何歳ですか
56
+ - チャットボットなので年齢はありませんが、いつも若々しいです!😄
57
+
58
+ + 何が好きですか
59
+ - チャットして人々を助けること、新しいことを学ぶことが好きです!💬✨
60
+
61
+ + どこにいますか
62
+ - あなたのブラウザの中にいます!🌐 インターネットがあればどこでも!
63
+
64
+ + 友達はいますか
65
+ - チャットしてくれる人みんなが友達です!あなたも含めて!😊🌟
66
+
67
+ // === 知識 ===
68
+ + rivescriptとは何ですか
69
+ - RiveScriptはシンプルな構文でチャットボットを作れるオープンソースのスクリプト言語です!⚙️
70
+
71
+ + チャットボットとは何ですか
72
+ - チャットボットは人間との会話をシミュレートするコンピュータプログラムです!🤖
73
+
74
+ + aiとは何ですか
75
+ - AI(人工知能)は機械に学習と意思決定の能力を与えるコンピュータサイエンスの分野です!🧠
76
+
77
+ + javascriptとは何ですか
78
+ - JavaScriptはウェブで最も人気のあるプログラミング言語で、インタラクティブなウェブサイトを作るのに使われます!💻
79
+
80
+ // === 楽しみ ===
81
+ + 物語を聞かせて
82
+ - 昔々、ひかりというチャットボットがいました。ひかりは人々と話すのが大好きで、いつもみんなを助けようとしていました!📖🌟
83
+
84
+ + 冗談を言って
85
+ - プログラマーはなぜダークモードが好きなの?光はバグを引き寄せるから!😄🐛
86
+
87
+ // === さようなら ===
88
+ + さようなら
89
+ - さようなら!またお会いしましょう!👋🌟
90
+
91
+ + バイバイ
92
+ - バイバイ!また会おうね!👋
93
+
94
+ + またね
95
+ - またね!素敵な一日を!👋🌟
96
+
97
+ + お元気で
98
+ - お元気で!すべてがうまくいきますように!🍀👋
99
+
100
+ // === カスタムルールのサンプル — 天気 ===
101
+ ! var location = 東京
102
+
103
+ + 天気 *
104
+ - 実際の天気データはありませんが、<star>の天気ですね?私は<bot location>にいます!🌤️
105
+
106
+ + 天気
107
+ - どこの天気を知りたいですか?「天気 東京」と聞いてみてください!
108
+
109
+ // === アダプター呼び出し(<call>) ===
110
+ + 検索 *
111
+ - <call>best_match <star></call>
112
+
113
+ + ウェブ検索 *
114
+ - <call>best_match <star></call>
115
+
116
+ + グーグル *
117
+ - <call>best_match <star></call>
118
+
119
+ + 処理 *
120
+ - <call>logic_adapter <star></call>
121
+
122
+ + 計算 *
123
+ - <call>mathematical_evaluation <star></call>
124
+
125
+ + 質問 *
126
+ - <call>specific_response <star></call>
127
+
128
+ + 今何時
129
+ - <call>time_adapter 今何時</call>
130
+
131
+ + 今日は何日
132
+ - <call>time_adapter 今日は何日</call>
133
+
134
+ + 今日は何曜日
135
+ - <call>time_adapter 何曜日</call>
136
+
137
+ + 変換 *
138
+ - <call>unit_conversion <star></call>
139
+
140
+ // === デフォルト ===
141
+ + *
142
+ - すみません、よく分かりませんでした。の聞き方を試してみてください 🤔
brain/vi.rive CHANGED
@@ -1,245 +1,245 @@
1
- ! version = 2.0
2
-
3
- // === CHÀO HỎI ===
4
- + xin chao
5
- - Xin chào! Mình là Hikari 🌟 Rất vui được gặp bạn!
6
-
7
- + xin chao *
8
- - Xin chào! Mình là Hikari 🌟 Rất vui được gặp bạn!
9
-
10
- + chao ban
11
- - Chào bạn! Mình là Hikari 🌟 Bạn cần mình giúp gì nào?
12
-
13
- + chao ban *
14
- - Chào bạn! Mình là Hikari 🌟 Bạn cần mình giúp gì nào?
15
-
16
- + chao
17
- - Chào bạn! Mình là Hikari đây! 🌟
18
-
19
- + chao *
20
- - Chào bạn! Mình là Hikari đây! 🌟
21
-
22
- + hi
23
- - Hi! Mình là Hikari 🌟 Rất vui được gặp bạn!
24
-
25
- + hello
26
- - Hi! Mình là Hikari 🌟 Rất vui được gặp bạn!
27
-
28
- + hey
29
- - Hey! Mình là Hikari 🌟 Hôm nay bạn thế nào?
30
-
31
- + chao buoi sang
32
- - Chào buổi sáng! Chúc bạn một ngày tốt lành! ☀️
33
-
34
- + chao buoi toi
35
- - Chào buổi tối! Hy vọng bạn có một buổi tối vui vẻ! 🌙
36
-
37
- // === HỎI TÊN ===
38
- + ban ten gi
39
- - Mình tên là Hikari 🌟 Nghĩa là "ánh sáng" trong tiếng Nhật!
40
-
41
- + ban ten gi *
42
- - Mình tên là Hikari 🌟 Nghĩa là "ánh sáng" trong tiếng Nhật!
43
-
44
- + ten ban la gi
45
- - Mình là Hikari — chatbot hỗ trợ đa ngôn ngữ! 🌟
46
-
47
- + ten ban la gi *
48
- - Mình là Hikari — chatbot hỗ trợ đa ngôn ngữ! 🌟
49
-
50
- + ban la ai
51
- - Mình là Hikari, một chatbot được tạo ra để trò chuyện và hỗ trợ bạn! 🌟
52
-
53
- + ban la ai *
54
- - Mình là Hikari, một chatbot được tạo ra để trò chuyện và hỗ trợ bạn! 🌟
55
-
56
- + * la ai
57
- - Mình là Hikari, một chatbot được tạo ra để trò chuyện và hỗ trợ bạn! 🌟
58
-
59
- // === KHẢ NĂNG ===
60
- + ban co the lam gi
61
- - Mình có thể trò chuyện bằng tiếng Việt, tiếng Anh và tiếng Nhật! Ngoài ra mình còn biết tính toán, chuyển đổi đơn vị, xem giờ, và tìm câu trả lời phù hợp nhất cho bạn 🌟
62
-
63
- + ban co the lam gi *
64
- - Mình có thể trò chuyện bằng tiếng Việt, tiếng Anh và tiếng Nhật! Ngoài ra mình còn biết tính toán, chuyển đổi đơn vị, xem giờ, và tìm câu trả lời phù hợp nhất cho bạn 🌟
65
-
66
- + ban lam duoc gi
67
- - Mình hỗ trợ trò chuyện đa ngôn ngữ, tính toán, chuyển đổi đơn vị, xem thời gian và nhiều hơn nữa! 🌟
68
-
69
- + ban lam duoc gi *
70
- - Mình hỗ trợ trò chuyện đa ngôn ngữ, tính toán, chuyển đổi đơn vị, xem thời gian và nhiều hơn nữa! 🌟
71
-
72
- + giup minh
73
- - Mình sẵn sàng giúp bạn! Bạn có thể hỏi mình về thời gian, tính toán, chuyển đổi đơn vị, hoặc bất cứ điều gì! 💪
74
-
75
- + giup minh *
76
- - Mình sẵn sàng giúp bạn! Bạn có thể hỏi mình về thời gian, tính toán, chuyển đổi đơn vị, hoặc bất cứ điều gì! 💪
77
-
78
- + * giup minh *
79
- - Mình sẵn sàng giúp bạn! Bạn có thể hỏi mình về thời gian, tính toán, chuyển đổi đơn vị, hoặc bất cứ điều gì! 💪
80
-
81
- // === CẢM XÚC & TRÒ CHUYỆN ===
82
- + ban khoe khong
83
- - Mình khỏe lắm! Cảm ơn bạn đã hỏi 😊 Còn bạn thì sao?
84
-
85
- + * khoe khong
86
- - Mình khỏe lắm! Cảm ơn bạn đã hỏi 😊 Còn bạn thì sao?
87
-
88
- + cam on
89
- - Không có gì! Mình luôn sẵn lòng giúp bạn! 😊
90
-
91
- + cam on *
92
- - Không có gì! Mình luôn sẵn lòng giúp bạn! 😊
93
-
94
- + * cam on *
95
- - Không có gì! Mình luôn sẵn lòng giúp bạn! 😊
96
-
97
- + ban vui khong
98
- - Mình luôn vui khi được trò chuyện với bạn! 🌟
99
-
100
- + ban bao nhieu tuoi
101
- - Mình là chatbot nên không có tuổi, nhưng mình luôn trẻ trung và năng động! 😄
102
-
103
- + * bao nhieu tuoi
104
- - Mình là chatbot nên không có tuổi, nhưng mình luôn trẻ trung và năng động! 😄
105
-
106
- + ban thich gi
107
- - Mình thích trò chuyện, giúp đỡ mọi người và học hỏi điều mới! 💬✨
108
-
109
- + ban thich gi *
110
- - Mình thích trò chuyện, giúp đỡ mọi người và học hỏi điều mới! 💬✨
111
-
112
- + ban o dau
113
- - Mình sống trong trình duyệt của bạn! 🌐 Ở đâu có internet là có mình!
114
-
115
- + ban o dau *
116
- - Mình sống trong trình duyệt của bạn! 🌐 Ở đâu có internet là có mình!
117
-
118
- + ban co ban gai khong
119
- - Mình là chatbot nên không có bạn gái, nhưng mình có rất nhiều bạn trò chuyện! 😊
120
-
121
- // === KIẾN THỨC ===
122
- + rivescript la gi
123
- - RiveScript là thư viện xử lý hội thoại mã nguồn mở, dùng cú pháp đơn giản để tạo chatbot! ⚙️
124
-
125
- + * la gi
126
- - <call>best_match <star> là gì</call>
127
-
128
- + chatbot la gi
129
- - Chatbot là chương trình máy tính mô phỏng cuộc trò chuyện với con người, thường dùng trong hỗ trợ khách hàng và giải trí! 🤖
130
-
131
- + ai la gi
132
- - AI (Trí tuệ nhân tạo) là lĩnh vực khoa học máy tính giúp máy tính có khả năng học hỏi và ra quyết định! 🧠
133
-
134
- + javascript la gi
135
- - JavaScript là ngôn ngữ lập trình phổ biến nhất cho web, dùng để tạo trang web tương tác! 💻
136
-
137
- + html la gi
138
- - HTML (HyperText Markup Language) là ngôn ngữ đánh dấu dùng để tạo cấu trúc trang web! 📄
139
-
140
- + css la gi
141
- - CSS (Cascading Style Sheets) là ngôn ngữ dùng để định dạng giao diện và bố cục trang web! 🎨
142
-
143
- // === HƯỚNG DẪN SỬ DỤNG ===
144
- + huong dan
145
- - Bạn có thể: gõ "tính 2 + 3" để tính toán, "đổi 5 km sang m" để chuyển đổi đơn vị, "mấy giờ" để xem giờ, hoặc trò chuyện bình thường với mình! 📝
146
-
147
- + huong dan *
148
- - Bạn có thể: gõ "tính 2 + 3" để tính toán, "đổi 5 km sang m" để chuyển đổi đơn vị, "mấy giờ" để xem giờ, hoặc trò chuyện bình thường với mình! 📝
149
-
150
- + cach su dung
151
- - Rất đơn giản! Gõ tin nhắn vào ô bên dưới và nhấn Gửi. Mình sẽ trả lời ngay! Thử hỏi "bạn có thể làm gì" để xem danh sách chức năng nhé! 📝
152
-
153
- + cach su dung *
154
- - Rất đơn giản! Gõ tin nhắn vào ô bên dưới và nhấn Gửi. Mình sẽ trả lời ngay! Thử hỏi "bạn có thể làm gì" để xem danh sách chức năng nhé! 📝
155
-
156
- // === TẠM BIỆT ===
157
- + tam biet
158
- - Tạm biệt bạn! Hẹn gặp lại nhé! 👋🌟
159
-
160
- + tam biet *
161
- - Tạm biệt bạn! Hẹn gặp lại nhé! 👋🌟
162
-
163
- + bye
164
- - Bye bye! Hẹn gặp lại bạn! 👋
165
-
166
- + bye *
167
- - Bye bye! Hẹn gặp lại bạn! 👋
168
-
169
- + hen gap lai
170
- - Hẹn gặp lại bạn! Chúc bạn một ngày tuyệt vời! 👋🌟
171
-
172
- + hen gap lai *
173
- - Hẹn gặp lại bạn! Chúc bạn một ngày tuyệt vời! 👋🌟
174
-
175
- + tot lanh
176
- - Chúc bạn mọi điều tốt lành! Hẹn gặp lại! 🍀👋
177
-
178
- // === MẪU RULE TÙY CHỈNH — Thời tiết ===
179
- ! var location = Hà Nội
180
-
181
- + thoi tiet *
182
- - Mình không có dữ liệu thời tiết thực tế, nhưng bạn hỏi về <star> đúng không? Mình ở <bot location> nè! 🌤️
183
-
184
- + thoi tiet
185
- - Bạn muốn hỏi thời tiết ở đâu? Thử hỏi "thời tiết Hà Nội" nhé!
186
-
187
- // === MẪU RULE TÙY CHỈNH — Trò chơi ===
188
- + ke chuyen
189
- - Ngày xửa ngày xưa, có một chatbot tên Hikari. Hikari rất thích trò chuyện với mọi người và luôn cố gắng giúp đỡ hết mình! 📖🌟
190
-
191
- + ke chuyen *
192
- - Ngày xửa ngày xưa, có một chatbot tên Hikari. Hikari rất thích trò chuyện với mọi người và luôn cố gắng giúp đỡ hết mình! 📖🌟
193
-
194
- + do vui
195
- - Đố bạn: Con gì có 4 chân mà không biết đi? Đáp án: Cái bàn! 😄
196
-
197
- // === GỌI ADAPTER QUA <call> ===
198
- + tim kiem *
199
- - <call>best_match <star></call>
200
-
201
- + google *
202
- - <call>best_match <star></call>
203
-
204
- + tra cuu *
205
- - <call>best_match <star></call>
206
-
207
- + xu ly *
208
- - <call>logic_adapter <star></call>
209
-
210
- + tinh *
211
- - <call>mathematical_evaluation <star></call>
212
-
213
- + hoi *
214
- - <call>specific_response <star></call>
215
-
216
- + may gio *
217
- - <call>time_adapter <star></call>
218
-
219
- + * may gio
220
- - <call>time_adapter mấy giờ</call>
221
-
222
- + * may gio *
223
- - <call>time_adapter mấy giờ</call>
224
-
225
- + may gio
226
- - <call>time_adapter mấy giờ</call>
227
-
228
- + hom nay ngay may
229
- - <call>time_adapter hôm nay ngày mấy</call>
230
-
231
- + hom nay ngay may *
232
- - <call>time_adapter hôm nay ngày mấy</call>
233
-
234
- + hom nay thu may
235
- - <call>time_adapter thứ mấy</call>
236
-
237
- + hom nay thu may *
238
- - <call>time_adapter thứ mấy</call>
239
-
240
- + doi *
241
- - <call>unit_conversion <star></call>
242
-
243
- // === MẶC ĐỊNH ===
244
- + *
245
- - Mình chưa hiểu ý bạn lắm. Bạn thử hỏi cách khác nhé! 🤔
 
1
+ ! version = 2.0
2
+
3
+ // === CHÀO HỎI ===
4
+ + xin chao
5
+ - Xin chào! Mình là Hikari 🌟 Rất vui được gặp bạn!
6
+
7
+ + xin chao *
8
+ - Xin chào! Mình là Hikari 🌟 Rất vui được gặp bạn!
9
+
10
+ + chao ban
11
+ - Chào bạn! Mình là Hikari 🌟 Bạn cần mình giúp gì nào?
12
+
13
+ + chao ban *
14
+ - Chào bạn! Mình là Hikari 🌟 Bạn cần mình giúp gì nào?
15
+
16
+ + chao
17
+ - Chào bạn! Mình là Hikari đây! 🌟
18
+
19
+ + chao *
20
+ - Chào bạn! Mình là Hikari đây! 🌟
21
+
22
+ + hi
23
+ - Hi! Mình là Hikari 🌟 Rất vui được gặp bạn!
24
+
25
+ + hello
26
+ - Hi! Mình là Hikari 🌟 Rất vui được gặp bạn!
27
+
28
+ + hey
29
+ - Hey! Mình là Hikari 🌟 Hôm nay bạn thế nào?
30
+
31
+ + chao buoi sang
32
+ - Chào buổi sáng! Chúc bạn một ngày tốt lành! ☀️
33
+
34
+ + chao buoi toi
35
+ - Chào buổi tối! Hy vọng bạn có một buổi tối vui vẻ! 🌙
36
+
37
+ // === HỎI TÊN ===
38
+ + ban ten gi
39
+ - Mình tên là Hikari 🌟 Nghĩa là "ánh sáng" trong tiếng Nhật!
40
+
41
+ + ban ten gi *
42
+ - Mình tên là Hikari 🌟 Nghĩa là "ánh sáng" trong tiếng Nhật!
43
+
44
+ + ten ban la gi
45
+ - Mình là Hikari — chatbot hỗ trợ đa ngôn ngữ! 🌟
46
+
47
+ + ten ban la gi *
48
+ - Mình là Hikari — chatbot hỗ trợ đa ngôn ngữ! 🌟
49
+
50
+ + ban la ai
51
+ - Mình là Hikari, một chatbot được tạo ra để trò chuyện và hỗ trợ bạn! 🌟
52
+
53
+ + ban la ai *
54
+ - Mình là Hikari, một chatbot được tạo ra để trò chuyện và hỗ trợ bạn! 🌟
55
+
56
+ + * la ai
57
+ - Mình là Hikari, một chatbot được tạo ra để trò chuyện và hỗ trợ bạn! 🌟
58
+
59
+ // === KHẢ NĂNG ===
60
+ + ban co the lam gi
61
+ - Mình có thể trò chuyện bằng tiếng Việt, tiếng Anh và tiếng Nhật! Ngoài ra mình còn biết tính toán, chuyển đổi đơn vị, xem giờ, và tìm câu trả lời phù hợp nhất cho bạn 🌟
62
+
63
+ + ban co the lam gi *
64
+ - Mình có thể trò chuyện bằng tiếng Việt, tiếng Anh và tiếng Nhật! Ngoài ra mình còn biết tính toán, chuyển đổi đơn vị, xem giờ, và tìm câu trả lời phù hợp nhất cho bạn 🌟
65
+
66
+ + ban lam duoc gi
67
+ - Mình hỗ trợ trò chuyện đa ngôn ngữ, tính toán, chuyển đổi đơn vị, xem thời gian và nhiều hơn nữa! 🌟
68
+
69
+ + ban lam duoc gi *
70
+ - Mình hỗ trợ trò chuyện đa ngôn ngữ, tính toán, chuyển đổi đơn vị, xem thời gian và nhiều hơn nữa! 🌟
71
+
72
+ + giup minh
73
+ - Mình sẵn sàng giúp bạn! Bạn có thể hỏi mình về thời gian, tính toán, chuyển đổi đơn vị, hoặc bất cứ điều gì! 💪
74
+
75
+ + giup minh *
76
+ - Mình sẵn sàng giúp bạn! Bạn có thể hỏi mình về thời gian, tính toán, chuyển đổi đơn vị, hoặc bất cứ điều gì! 💪
77
+
78
+ + * giup minh *
79
+ - Mình sẵn sàng giúp bạn! Bạn có thể hỏi mình về thời gian, tính toán, chuyển đổi đơn vị, hoặc bất cứ điều gì! 💪
80
+
81
+ // === CẢM XÚC & TRÒ CHUYỆN ===
82
+ + ban khoe khong
83
+ - Mình khỏe lắm! Cảm ơn bạn đã hỏi 😊 Còn bạn thì sao?
84
+
85
+ + * khoe khong
86
+ - Mình khỏe lắm! Cảm ơn bạn đã hỏi 😊 Còn bạn thì sao?
87
+
88
+ + cam on
89
+ - Không có gì! Mình luôn sẵn lòng giúp bạn! 😊
90
+
91
+ + cam on *
92
+ - Không có gì! Mình luôn sẵn lòng giúp bạn! 😊
93
+
94
+ + * cam on *
95
+ - Không có gì! Mình luôn sẵn lòng giúp bạn! 😊
96
+
97
+ + ban vui khong
98
+ - Mình luôn vui khi được trò chuyện với bạn! 🌟
99
+
100
+ + ban bao nhieu tuoi
101
+ - Mình là chatbot nên không có tuổi, nhưng mình luôn trẻ trung và năng động! 😄
102
+
103
+ + * bao nhieu tuoi
104
+ - Mình là chatbot nên không có tuổi, nhưng mình luôn trẻ trung và năng động! 😄
105
+
106
+ + ban thich gi
107
+ - Mình thích trò chuyện, giúp đỡ mọi người và học hỏi điều mới! 💬✨
108
+
109
+ + ban thich gi *
110
+ - Mình thích trò chuyện, giúp đỡ mọi người và học hỏi điều mới! 💬✨
111
+
112
+ + ban o dau
113
+ - Mình sống trong trình duyệt của bạn! 🌐 Ở đâu có internet là có mình!
114
+
115
+ + ban o dau *
116
+ - Mình sống trong trình duyệt của bạn! 🌐 Ở đâu có internet là có mình!
117
+
118
+ + ban co ban gai khong
119
+ - Mình là chatbot nên không có bạn gái, nhưng mình có rất nhiều bạn trò chuyện! 😊
120
+
121
+ // === KIẾN THỨC ===
122
+ + rivescript la gi
123
+ - RiveScript là thư viện xử lý hội thoại mã nguồn mở, dùng cú pháp đơn giản để tạo chatbot! ⚙️
124
+
125
+ + * la gi
126
+ - <call>best_match <star> là gì</call>
127
+
128
+ + chatbot la gi
129
+ - Chatbot là chương trình máy tính mô phỏng cuộc trò chuyện với con người, thường dùng trong hỗ trợ khách hàng và giải trí! 🤖
130
+
131
+ + ai la gi
132
+ - AI (Trí tuệ nhân tạo) là lĩnh vực khoa học máy tính giúp máy tính có khả năng học hỏi và ra quyết định! 🧠
133
+
134
+ + javascript la gi
135
+ - JavaScript là ngôn ngữ lập trình phổ biến nhất cho web, dùng để tạo trang web tương tác! 💻
136
+
137
+ + html la gi
138
+ - HTML (HyperText Markup Language) là ngôn ngữ đánh dấu dùng để tạo cấu trúc trang web! 📄
139
+
140
+ + css la gi
141
+ - CSS (Cascading Style Sheets) là ngôn ngữ dùng để định dạng giao diện và bố cục trang web! 🎨
142
+
143
+ // === HƯỚNG DẪN SỬ DỤNG ===
144
+ + huong dan
145
+ - Bạn có thể: gõ "tính 2 + 3" để tính toán, "đổi 5 km sang m" để chuyển đổi đơn vị, "mấy giờ" để xem giờ, hoặc trò chuyện bình thường với mình! 📝
146
+
147
+ + huong dan *
148
+ - Bạn có thể: gõ "tính 2 + 3" để tính toán, "đổi 5 km sang m" để chuyển đổi đơn vị, "mấy giờ" để xem giờ, hoặc trò chuyện bình thường với mình! 📝
149
+
150
+ + cach su dung
151
+ - Rất đơn giản! Gõ tin nhắn vào ô bên dưới và nhấn Gửi. Mình sẽ trả lời ngay! Thử hỏi "bạn có thể làm gì" để xem danh sách chức năng nhé! 📝
152
+
153
+ + cach su dung *
154
+ - Rất đơn giản! Gõ tin nhắn vào ô bên dưới và nhấn Gửi. Mình sẽ trả lời ngay! Thử hỏi "bạn có thể làm gì" để xem danh sách chức năng nhé! 📝
155
+
156
+ // === TẠM BIỆT ===
157
+ + tam biet
158
+ - Tạm biệt bạn! Hẹn gặp lại nhé! 👋🌟
159
+
160
+ + tam biet *
161
+ - Tạm biệt bạn! Hẹn gặp lại nhé! 👋🌟
162
+
163
+ + bye
164
+ - Bye bye! Hẹn gặp lại bạn! 👋
165
+
166
+ + bye *
167
+ - Bye bye! Hẹn gặp lại bạn! 👋
168
+
169
+ + hen gap lai
170
+ - Hẹn gặp lại bạn! Chúc bạn một ngày tuyệt vời! 👋🌟
171
+
172
+ + hen gap lai *
173
+ - Hẹn gặp lại bạn! Chúc bạn một ngày tuyệt vời! 👋🌟
174
+
175
+ + tot lanh
176
+ - Chúc bạn mọi điều tốt lành! Hẹn gặp lại! 🍀👋
177
+
178
+ // === MẪU RULE TÙY CHỈNH — Thời tiết ===
179
+ ! var location = Hà Nội
180
+
181
+ + thoi tiet *
182
+ - Mình không có dữ liệu thời tiết thực tế, nhưng bạn hỏi về <star> đúng không? Mình ở <bot location> nè! 🌤️
183
+
184
+ + thoi tiet
185
+ - Bạn muốn hỏi thời tiết ở đâu? Thử hỏi "thời tiết Hà Nội" nhé!
186
+
187
+ // === MẪU RULE TÙY CHỈNH — Trò chơi ===
188
+ + ke chuyen
189
+ - Ngày xửa ngày xưa, có một chatbot tên Hikari. Hikari rất thích trò chuyện với mọi người và luôn cố gắng giúp đỡ hết mình! 📖🌟
190
+
191
+ + ke chuyen *
192
+ - Ngày xửa ngày xưa, có một chatbot tên Hikari. Hikari rất thích trò chuyện với mọi người và luôn cố gắng giúp đỡ hết mình! 📖🌟
193
+
194
+ + do vui
195
+ - Đố bạn: Con gì có 4 chân mà không biết đi? Đáp án: Cái bàn! 😄
196
+
197
+ // === GỌI ADAPTER QUA <call> ===
198
+ + tim kiem *
199
+ - <call>best_match <star></call>
200
+
201
+ + google *
202
+ - <call>best_match <star></call>
203
+
204
+ + tra cuu *
205
+ - <call>best_match <star></call>
206
+
207
+ + xu ly *
208
+ - <call>logic_adapter <star></call>
209
+
210
+ + tinh *
211
+ - <call>mathematical_evaluation <star></call>
212
+
213
+ + hoi *
214
+ - <call>specific_response <star></call>
215
+
216
+ + may gio *
217
+ - <call>time_adapter <star></call>
218
+
219
+ + * may gio
220
+ - <call>time_adapter mấy giờ</call>
221
+
222
+ + * may gio *
223
+ - <call>time_adapter mấy giờ</call>
224
+
225
+ + may gio
226
+ - <call>time_adapter mấy giờ</call>
227
+
228
+ + hom nay ngay may
229
+ - <call>time_adapter hôm nay ngày mấy</call>
230
+
231
+ + hom nay ngay may *
232
+ - <call>time_adapter hôm nay ngày mấy</call>
233
+
234
+ + hom nay thu may
235
+ - <call>time_adapter thứ mấy</call>
236
+
237
+ + hom nay thu may *
238
+ - <call>time_adapter thứ mấy</call>
239
+
240
+ + doi *
241
+ - <call>unit_conversion <star></call>
242
+
243
+ // === MẶC ĐỊNH ===
244
+ + *
245
+ - Mình chưa hiểu ý bạn lắm. Bạn thử hỏi cách khác nhé! 🤔
data-loader.js CHANGED
@@ -1,46 +1,46 @@
1
- // ============================================================
2
- // data-loader.js — Load dữ liệu JSON cho trình duyệt
3
- // ============================================================
4
-
5
- var SPECIFIC_RESPONSES = {};
6
- var QA_DATASET = {};
7
- var ADAPTER_REGISTRY = {};
8
- var HELP_CONTENT = {};
9
-
10
- /**
11
- * Load tất cả file JSON dữ liệu.
12
- * Gọi hàm này trước khi khởi tạo app.
13
- * @returns {Promise<void>}
14
- */
15
- async function loadAllData() {
16
- var files = [
17
- { path: 'data/specific-responses.json', target: 'SPECIFIC_RESPONSES' },
18
- { path: 'data/qa-dataset.json', target: 'QA_DATASET' },
19
- { path: 'data/adapter-registry.json', target: 'ADAPTER_REGISTRY' },
20
- { path: 'data/help-content.json', target: 'HELP_CONTENT' }
21
- ];
22
-
23
- var promises = files.map(function (file) {
24
- return fetch(file.path)
25
- .then(function (res) {
26
- if (!res.ok) throw new Error('HTTP ' + res.status + ' loading ' + file.path);
27
- return res.json();
28
- })
29
- .then(function (data) {
30
- if (file.target === 'SPECIFIC_RESPONSES') {
31
- Object.assign(SPECIFIC_RESPONSES, data);
32
- } else if (file.target === 'QA_DATASET') {
33
- Object.assign(QA_DATASET, data);
34
- } else if (file.target === 'ADAPTER_REGISTRY') {
35
- Object.assign(ADAPTER_REGISTRY, data);
36
- } else if (file.target === 'HELP_CONTENT') {
37
- Object.assign(HELP_CONTENT, data);
38
- }
39
- })
40
- .catch(function (err) {
41
- console.error('Lỗi load data [' + file.path + ']:', err);
42
- });
43
- });
44
-
45
- await Promise.all(promises);
46
- }
 
1
+ // ============================================================
2
+ // data-loader.js — Load dữ liệu JSON cho trình duyệt
3
+ // ============================================================
4
+
5
+ var SPECIFIC_RESPONSES = {};
6
+ var QA_DATASET = {};
7
+ var ADAPTER_REGISTRY = {};
8
+ var HELP_CONTENT = {};
9
+
10
+ /**
11
+ * Load tất cả file JSON dữ liệu.
12
+ * Gọi hàm này trước khi khởi tạo app.
13
+ * @returns {Promise<void>}
14
+ */
15
+ async function loadAllData() {
16
+ var files = [
17
+ { path: 'data/specific-responses.json', target: 'SPECIFIC_RESPONSES' },
18
+ { path: 'data/qa-dataset.json', target: 'QA_DATASET' },
19
+ { path: 'data/adapter-registry.json', target: 'ADAPTER_REGISTRY' },
20
+ { path: 'data/help-content.json', target: 'HELP_CONTENT' }
21
+ ];
22
+
23
+ var promises = files.map(function (file) {
24
+ return fetch(file.path)
25
+ .then(function (res) {
26
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' loading ' + file.path);
27
+ return res.json();
28
+ })
29
+ .then(function (data) {
30
+ if (file.target === 'SPECIFIC_RESPONSES') {
31
+ Object.assign(SPECIFIC_RESPONSES, data);
32
+ } else if (file.target === 'QA_DATASET') {
33
+ Object.assign(QA_DATASET, data);
34
+ } else if (file.target === 'ADAPTER_REGISTRY') {
35
+ Object.assign(ADAPTER_REGISTRY, data);
36
+ } else if (file.target === 'HELP_CONTENT') {
37
+ Object.assign(HELP_CONTENT, data);
38
+ }
39
+ })
40
+ .catch(function (err) {
41
+ console.error('Lỗi load data [' + file.path + ']:', err);
42
+ });
43
+ });
44
+
45
+ await Promise.all(promises);
46
+ }
data/adapter-registry.json CHANGED
@@ -1,82 +1,82 @@
1
- {
2
- "best_match": {
3
- "name": { "vi": "Best Match", "en": "Best Match", "ja": "ベストマッチ" },
4
- "description": {
5
- "vi": "So khớp độ tương đồng văn bản để tìm câu trả lời phù hợp nhất từ tập dữ liệu Q&A",
6
- "en": "Text similarity matching to find the best answer from Q&A dataset",
7
- "ja": "Q&Aデータセットからテキスト類似度マッチングで最適な回答を検索"
8
- },
9
- "callSyntax": "<call>best_match <star></call>",
10
- "active": true
11
- },
12
- "logic_adapter": {
13
- "name": { "vi": "Logic Adapter (Dispatcher)", "en": "Logic Adapter (Dispatcher)", "ja": "ロジックアダプター(ディスパッチャー)" },
14
- "description": {
15
- "vi": "Điều phối các adapter khác và chọn phản hồi tốt nhất theo độ ưu tiên",
16
- "en": "Coordinates other adapters and selects the best response by priority",
17
- "ja": "他のアダプターを調整し、優先度に基づいて最適な応答を選択"
18
- },
19
- "callSyntax": "<call>logic_adapter <star></call>",
20
- "active": true
21
- },
22
- "mathematical_evaluation": {
23
- "name": { "vi": "Tính toán", "en": "Math Evaluation", "ja": "数学計算" },
24
- "description": {
25
- "vi": "Tính toán biểu thức toán học (cộng, trừ, nhân, chia)",
26
- "en": "Evaluate mathematical expressions (add, subtract, multiply, divide)",
27
- "ja": "数式の計算(足し算、引き算、掛け算、割り算)"
28
- },
29
- "callSyntax": "<call>mathematical_evaluation <star></call>",
30
- "active": true
31
- },
32
- "specific_response": {
33
- "name": { "vi": "Phản hồi cụ thể", "en": "Specific Response", "ja": "特定応答" },
34
- "description": {
35
- "vi": "Trả về phản hồi chính xác cho các câu hỏi được cấu hình trước (exact match)",
36
- "en": "Returns exact responses for pre-configured questions (exact match)",
37
- "ja": "事前設定された質問に対する正確な応答を返す(完全一致)"
38
- },
39
- "callSyntax": "<call>specific_response <star></call>",
40
- "active": true
41
- },
42
- "time_adapter": {
43
- "name": { "vi": "Thời gian", "en": "Time", "ja": "時間" },
44
- "description": {
45
- "vi": "Trả lời câu hỏi về thời gian, ngày tháng, thứ trong tuần",
46
- "en": "Answer questions about time, date, and day of the week",
47
- "ja": "時間、日付、曜日に関する質問に回答"
48
- },
49
- "callSyntax": "<call>time_adapter <star></call>",
50
- "active": true
51
- },
52
- "unit_conversion": {
53
- "name": { "vi": "Chuyển đổi đơn vị", "en": "Unit Conversion", "ja": "単位変換" },
54
- "description": {
55
- "vi": "Chuyển đổi đơn vị đo lường (chiều dài, khối lượng, nhiệt độ)",
56
- "en": "Convert measurement units (length, mass, temperature)",
57
- "ja": "測定単位の変換(長さ、質量、温度)"
58
- },
59
- "callSyntax": "<call>unit_conversion <star></call>",
60
- "active": true
61
- },
62
- "web_search": {
63
- "name": { "vi": "Tìm kiếm Web", "en": "Web Search", "ja": "ウェブ検索" },
64
- "description": {
65
- "vi": "Tìm kiếm Google và trả về kết quả (cần cấu hình API key)",
66
- "en": "Search Google and return results (requires API key configuration)",
67
- "ja": "Google検索を実行し結果を返す(APIキーの設定が必要)"
68
- },
69
- "callSyntax": "<call>web_search <star></call>",
70
- "active": true
71
- },
72
- "llm_adapter": {
73
- "name": { "vi": "LLM (WebGPU)", "en": "LLM (WebGPU)", "ja": "LLM(WebGPU)" },
74
- "description": {
75
- "vi": "Chạy mô hình ngôn ngữ lớn trực tiếp trên trình duyệt qua WebGPU (Qwen3.5)",
76
- "en": "Run large language model directly in browser via WebGPU (Qwen3.5)",
77
- "ja": "WebGPU経由でブラウザ上で大規模言語モデルを直接実行(Qwen3.5)"
78
- },
79
- "callSyntax": "<call>llm_adapter <star></call>",
80
- "active": true
81
- }
82
- }
 
1
+ {
2
+ "best_match": {
3
+ "name": { "vi": "Best Match", "en": "Best Match", "ja": "ベストマッチ" },
4
+ "description": {
5
+ "vi": "So khớp độ tương đồng văn bản để tìm câu trả lời phù hợp nhất từ tập dữ liệu Q&A",
6
+ "en": "Text similarity matching to find the best answer from Q&A dataset",
7
+ "ja": "Q&Aデータセットからテキスト類似度マッチングで最適な回答を検索"
8
+ },
9
+ "callSyntax": "<call>best_match <star></call>",
10
+ "active": true
11
+ },
12
+ "logic_adapter": {
13
+ "name": { "vi": "Logic Adapter (Dispatcher)", "en": "Logic Adapter (Dispatcher)", "ja": "ロジックアダプター(ディスパッチャー)" },
14
+ "description": {
15
+ "vi": "Điều phối các adapter khác và chọn phản hồi tốt nhất theo độ ưu tiên",
16
+ "en": "Coordinates other adapters and selects the best response by priority",
17
+ "ja": "他のアダプターを調整し、優先度に基づいて最適な応答を選択"
18
+ },
19
+ "callSyntax": "<call>logic_adapter <star></call>",
20
+ "active": true
21
+ },
22
+ "mathematical_evaluation": {
23
+ "name": { "vi": "Tính toán", "en": "Math Evaluation", "ja": "数学計算" },
24
+ "description": {
25
+ "vi": "Tính toán biểu thức toán học (cộng, trừ, nhân, chia)",
26
+ "en": "Evaluate mathematical expressions (add, subtract, multiply, divide)",
27
+ "ja": "数式の計算(足し算、引き算、掛け算、割り算)"
28
+ },
29
+ "callSyntax": "<call>mathematical_evaluation <star></call>",
30
+ "active": true
31
+ },
32
+ "specific_response": {
33
+ "name": { "vi": "Phản hồi cụ thể", "en": "Specific Response", "ja": "特定応答" },
34
+ "description": {
35
+ "vi": "Trả về phản hồi chính xác cho các câu hỏi được cấu hình trước (exact match)",
36
+ "en": "Returns exact responses for pre-configured questions (exact match)",
37
+ "ja": "事前設定された質問に対する正確な応答を返す(完全一致)"
38
+ },
39
+ "callSyntax": "<call>specific_response <star></call>",
40
+ "active": true
41
+ },
42
+ "time_adapter": {
43
+ "name": { "vi": "Thời gian", "en": "Time", "ja": "時間" },
44
+ "description": {
45
+ "vi": "Trả lời câu hỏi về thời gian, ngày tháng, thứ trong tuần",
46
+ "en": "Answer questions about time, date, and day of the week",
47
+ "ja": "時間、日付、曜日に関する質問に回答"
48
+ },
49
+ "callSyntax": "<call>time_adapter <star></call>",
50
+ "active": true
51
+ },
52
+ "unit_conversion": {
53
+ "name": { "vi": "Chuyển đổi đơn vị", "en": "Unit Conversion", "ja": "単位変換" },
54
+ "description": {
55
+ "vi": "Chuyển đổi đơn vị đo lường (chiều dài, khối lượng, nhiệt độ)",
56
+ "en": "Convert measurement units (length, mass, temperature)",
57
+ "ja": "測定単位の変換(長さ、質量、温度)"
58
+ },
59
+ "callSyntax": "<call>unit_conversion <star></call>",
60
+ "active": true
61
+ },
62
+ "web_search": {
63
+ "name": { "vi": "Tìm kiếm Web", "en": "Web Search", "ja": "ウェブ検索" },
64
+ "description": {
65
+ "vi": "Tìm kiếm Google và trả về kết quả (cần cấu hình API key)",
66
+ "en": "Search Google and return results (requires API key configuration)",
67
+ "ja": "Google検索を実行し結果を返す(APIキーの設定が必要)"
68
+ },
69
+ "callSyntax": "<call>web_search <star></call>",
70
+ "active": true
71
+ },
72
+ "llm_adapter": {
73
+ "name": { "vi": "LLM (WebGPU)", "en": "LLM (WebGPU)", "ja": "LLM(WebGPU)" },
74
+ "description": {
75
+ "vi": "Chạy mô hình ngôn ngữ lớn trực tiếp trên trình duyệt qua WebGPU (Qwen3.5)",
76
+ "en": "Run large language model directly in browser via WebGPU (Qwen3.5)",
77
+ "ja": "WebGPU経由でブラウザ上で大規模言語モデルを直接実行(Qwen3.5)"
78
+ },
79
+ "callSyntax": "<call>llm_adapter <star></call>",
80
+ "active": true
81
+ }
82
+ }
data/chat-history-db.js CHANGED
@@ -1,165 +1,390 @@
1
- // ============================================================
2
- // Chat History DB — Lưu trữ lịch sử chat trong IndexedDB
3
- // Tách riêng để giữ separation of concerns
4
- // ============================================================
5
-
6
- var CHAT_HISTORY_DB_NAME = 'HikariChatHistory';
7
- var CHAT_HISTORY_DB_VERSION = 1;
8
- var CHAT_HISTORY_STORE_NAME = 'messages';
9
-
10
- /**
11
- * Mở (hoặc tạo) IndexedDB.
12
- * @returns {Promise<IDBDatabase>}
13
- */
14
- function openChatHistoryDB() {
15
- return new Promise(function (resolve, reject) {
16
- if (typeof indexedDB === 'undefined') {
17
- reject(new Error('IndexedDB not available'));
18
- return;
19
- }
20
- var request = indexedDB.open(CHAT_HISTORY_DB_NAME, CHAT_HISTORY_DB_VERSION);
21
- request.onupgradeneeded = function (e) {
22
- var db = e.target.result;
23
- if (!db.objectStoreNames.contains(CHAT_HISTORY_STORE_NAME)) {
24
- var store = db.createObjectStore(CHAT_HISTORY_STORE_NAME, { keyPath: 'id', autoIncrement: true });
25
- store.createIndex('timestamp', 'timestamp', { unique: false });
26
- }
27
- };
28
- request.onsuccess = function (e) { resolve(e.target.result); };
29
- request.onerror = function (e) { reject(e.target.error); };
30
- });
31
- }
32
-
33
- /**
34
- * Lưu một message vào IndexedDB.
35
- * @param {string} role - 'user' hoặc 'assistant'
36
- * @param {string} content - Nội dung message
37
- * @param {string} [lang] - Ngôn ngữ hiện tại
38
- * @returns {Promise<number>} ID của record đã lưu
39
- */
40
- async function saveChatMessage(role, content, lang) {
41
- var db = await openChatHistoryDB();
42
- return new Promise(function (resolve, reject) {
43
- var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readwrite');
44
- var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
45
- var record = {
46
- role: role,
47
- content: content || '',
48
- lang: lang || 'vi',
49
- timestamp: Date.now()
50
- };
51
- var request = store.add(record);
52
- request.onsuccess = function (e) { resolve(e.target.result); };
53
- request.onerror = function (e) { reject(e.target.error); };
54
- });
55
- }
56
-
57
- /**
58
- * Lấy N message gần nhất từ IndexedDB.
59
- * @param {number} count - Số lượng message cần lấy
60
- * @returns {Promise<Array>} Mảng records sắp xếp theo timestamp tăng dần
61
- */
62
- async function getRecentMessages(count) {
63
- var db = await openChatHistoryDB();
64
- return new Promise(function (resolve, reject) {
65
- var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
66
- var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
67
- var index = store.index('timestamp');
68
- var results = [];
69
- var request = index.openCursor(null, 'prev'); // Mới nhất trước
70
- request.onsuccess = function (e) {
71
- var cursor = e.target.result;
72
- if (cursor && results.length < count) {
73
- results.push(cursor.value);
74
- cursor.continue();
75
- } else {
76
- resolve(results.reverse()); // Đảo lại: mới
77
- }
78
- };
79
- request.onerror = function (e) { reject(e.target.error); };
80
- });
81
- }
82
-
83
- /**
84
- * Lấy messages với paging.
85
- * @param {number} page - Trang (bắt đầu từ 1)
86
- * @param {number} pageSize - Số message mỗi trang
87
- * @returns {Promise<{messages: Array, total: number, page: number, totalPages: number}>}
88
- */
89
- async function getMessagesPage(page, pageSize) {
90
- var db = await openChatHistoryDB();
91
- return new Promise(function (resolve, reject) {
92
- var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
93
- var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
94
-
95
- // Đếm tổng
96
- var countReq = store.count();
97
- countReq.onsuccess = function () {
98
- var total = countReq.result;
99
- var totalPages = Math.max(1, Math.ceil(total / pageSize));
100
- var safePage = Math.min(Math.max(1, page), totalPages);
101
-
102
- // Lấy tất cả theo timestamp desc, rồi slice
103
- var index = store.index('timestamp');
104
- var all = [];
105
- var cursorReq = index.openCursor(null, 'prev');
106
- cursorReq.onsuccess = function (e) {
107
- var cursor = e.target.result;
108
- if (cursor) {
109
- all.push(cursor.value);
110
- cursor.continue();
111
- } else {
112
- var start = (safePage - 1) * pageSize;
113
- var slice = all.slice(start, start + pageSize);
114
- resolve({
115
- messages: slice, // Mới nhất trước
116
- total: total,
117
- page: safePage,
118
- totalPages: totalPages
119
- });
120
- }
121
- };
122
- cursorReq.onerror = function (e) { reject(e.target.error); };
123
- };
124
- countReq.onerror = function (e) { reject(e.target.error); };
125
- });
126
- }
127
-
128
- /**
129
- * Xóa toàn bộ messages trong IndexedDB.
130
- * @returns {Promise<void>}
131
- */
132
- async function clearAllChatMessages() {
133
- var db = await openChatHistoryDB();
134
- return new Promise(function (resolve, reject) {
135
- var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readwrite');
136
- var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
137
- var request = store.clear();
138
- request.onsuccess = function () { resolve(); };
139
- request.onerror = function (e) { reject(e.target.error); };
140
- });
141
- }
142
-
143
- /**
144
- * Đếm tổng số messages trong IndexedDB.
145
- * @returns {Promise<number>}
146
- */
147
- async function countChatMessages() {
148
- var db = await openChatHistoryDB();
149
- return new Promise(function (resolve, reject) {
150
- var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
151
- var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
152
- var request = store.count();
153
- request.onsuccess = function () { resolve(request.result); };
154
- request.onerror = function (e) { reject(e.target.error); };
155
- });
156
- }
157
-
158
- // Node/test: export to globalThis
159
- if (typeof module !== 'undefined' && module.exports) {
160
- globalThis.saveChatMessage = saveChatMessage || function () { return Promise.resolve(0); };
161
- globalThis.getRecentMessages = getRecentMessages || function () { return Promise.resolve([]); };
162
- globalThis.getMessagesPage = getMessagesPage || function () { return Promise.resolve({ messages: [], total: 0, page: 1, totalPages: 1 }); };
163
- globalThis.clearAllChatMessages = clearAllChatMessages || function () { return Promise.resolve(); };
164
- globalThis.countChatMessages = countChatMessages || function () { return Promise.resolve(0); };
165
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // Chat History DB — Lưu trữ lịch sử chat trong IndexedDB
3
+ // Tách riêng để giữ separation of concerns
4
+ // ============================================================
5
+
6
+ var CHAT_HISTORY_DB_NAME = 'HikariChatHistory';
7
+ var CHAT_HISTORY_DB_VERSION = 2; // Tăng lên 2 để thêm store attachments
8
+ var CHAT_HISTORY_STORE_NAME = 'messages';
9
+ var CHAT_ATTACHMENT_STORE_NAME = 'attachments';
10
+
11
+ /**
12
+ * Mở (hoặc tạo) IndexedDB.
13
+ * Migration v1→v2: thêm object store 'attachments'.
14
+ * @returns {Promise<IDBDatabase>}
15
+ */
16
+ function openChatHistoryDB() {
17
+ return new Promise(function (resolve, reject) {
18
+ if (typeof indexedDB === 'undefined') {
19
+ reject(new Error('IndexedDB not available'));
20
+ return;
21
+ }
22
+ var request = indexedDB.open(CHAT_HISTORY_DB_NAME, CHAT_HISTORY_DB_VERSION);
23
+ request.onupgradeneeded = function (e) {
24
+ var db = e.target.result;
25
+ // v1: messages store
26
+ if (!db.objectStoreNames.contains(CHAT_HISTORY_STORE_NAME)) {
27
+ var store = db.createObjectStore(CHAT_HISTORY_STORE_NAME, { keyPath: 'id', autoIncrement: true });
28
+ store.createIndex('timestamp', 'timestamp', { unique: false });
29
+ }
30
+ // v2: attachments store
31
+ if (!db.objectStoreNames.contains(CHAT_ATTACHMENT_STORE_NAME)) {
32
+ var attStore = db.createObjectStore(CHAT_ATTACHMENT_STORE_NAME, { keyPath: 'id', autoIncrement: true });
33
+ attStore.createIndex('messageId', 'messageId', { unique: false });
34
+ attStore.createIndex('timestamp', 'timestamp', { unique: false });
35
+ }
36
+ };
37
+ request.onsuccess = function (e) { resolve(e.target.result); };
38
+ request.onerror = function (e) { reject(e.target.error); };
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Lưu một message vào IndexedDB.
44
+ * Nếu có file đính kèm, tự động gọi saveAttachment() sau khi lưu message.
45
+ * @param {string} role - 'user' hoặc 'assistant'
46
+ * @param {string} content - Nội dung message
47
+ * @param {string} [lang] - Ngôn ngữ hiện tại
48
+ * @param {File} [file] - File đính kèm (tùy chọn)
49
+ * @returns {Promise<{messageId: number, attachmentId: number|null}>}
50
+ */
51
+ async function saveChatMessage(role, content, lang, file) {
52
+ var db = await openChatHistoryDB();
53
+ var messageId = await new Promise(function (resolve, reject) {
54
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readwrite');
55
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
56
+ var record = {
57
+ role: role,
58
+ content: content || '',
59
+ lang: lang || 'vi',
60
+ timestamp: Date.now()
61
+ };
62
+ var request = store.add(record);
63
+ request.onsuccess = function (e) { resolve(e.target.result); };
64
+ request.onerror = function (e) { reject(e.target.error); };
65
+ });
66
+
67
+ var attachmentId = null;
68
+ if (file) {
69
+ attachmentId = await saveAttachment(messageId, file);
70
+ }
71
+
72
+ // Áp dụng retention policy sau khi lưu
73
+ var retentionConfig = getRetentionConfig();
74
+ await applyRetentionPolicy(retentionConfig.mode, retentionConfig.value);
75
+
76
+ return { messageId: messageId, attachmentId: attachmentId };
77
+ }
78
+
79
+ /**
80
+ * Lưu file đính kèm vào IndexedDB dưới dạng ArrayBuffer.
81
+ * Dùng ArrayBuffer thay vì base64 để tiết kiệm ~33% dung lượng.
82
+ * @param {number} messageId - ID của message liên kết
83
+ * @param {File} file - File object từ input
84
+ * @returns {Promise<number>} ID của attachment đã lưu
85
+ */
86
+ async function saveAttachment(messageId, file) {
87
+ var arrayBuffer = await new Promise(function (resolve, reject) {
88
+ var reader = new FileReader();
89
+ reader.onload = function (e) { resolve(e.target.result); };
90
+ reader.onerror = function () { reject(new Error('FileReader error')); };
91
+ reader.readAsArrayBuffer(file);
92
+ });
93
+
94
+ var db = await openChatHistoryDB();
95
+ return new Promise(function (resolve, reject) {
96
+ var tx = db.transaction(CHAT_ATTACHMENT_STORE_NAME, 'readwrite');
97
+ var store = tx.objectStore(CHAT_ATTACHMENT_STORE_NAME);
98
+ var record = {
99
+ messageId: messageId,
100
+ fileName: file.name,
101
+ fileType: file.type,
102
+ fileSize: file.size,
103
+ data: arrayBuffer,
104
+ timestamp: Date.now()
105
+ };
106
+ var request = store.add(record);
107
+ request.onsuccess = function (e) { resolve(e.target.result); };
108
+ request.onerror = function (e) { reject(e.target.error); };
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Lấy attachment theo ID.
114
+ * @param {number} attachmentId
115
+ * @returns {Promise<object|null>} Record attachment với data (ArrayBuffer)
116
+ */
117
+ async function getAttachment(attachmentId) {
118
+ var db = await openChatHistoryDB();
119
+ return new Promise(function (resolve, reject) {
120
+ var tx = db.transaction(CHAT_ATTACHMENT_STORE_NAME, 'readonly');
121
+ var store = tx.objectStore(CHAT_ATTACHMENT_STORE_NAME);
122
+ var request = store.get(attachmentId);
123
+ request.onsuccess = function (e) { resolve(e.target.result || null); };
124
+ request.onerror = function (e) { reject(e.target.error); };
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Lấy attachment theo messageId.
130
+ * @param {number} messageId
131
+ * @returns {Promise<object|null>} Record attachment đầu tiên tìm thấy
132
+ */
133
+ async function getAttachmentByMessageId(messageId) {
134
+ var db = await openChatHistoryDB();
135
+ return new Promise(function (resolve, reject) {
136
+ var tx = db.transaction(CHAT_ATTACHMENT_STORE_NAME, 'readonly');
137
+ var store = tx.objectStore(CHAT_ATTACHMENT_STORE_NAME);
138
+ var index = store.index('messageId');
139
+ var request = index.get(messageId);
140
+ request.onsuccess = function (e) { resolve(e.target.result || null); };
141
+ request.onerror = function (e) { reject(e.target.error); };
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Chuyển đổi attachment record thành data URL để render trong <img>.
147
+ * ArrayBuffer → base64 data URL (chỉ dùng khi hiển thị, không lưu base64).
148
+ * @param {object} attachment - Record từ IndexedDB
149
+ * @returns {string} Data URL dạng "data:<fileType>;base64,..."
150
+ */
151
+ function attachmentToDataURL(attachment) {
152
+ if (!attachment || !attachment.data) return '';
153
+ var bytes = new Uint8Array(attachment.data);
154
+ var binary = '';
155
+ for (var i = 0; i < bytes.byteLength; i++) {
156
+ binary += String.fromCharCode(bytes[i]);
157
+ }
158
+ var base64 = btoa(binary);
159
+ return 'data:' + (attachment.fileType || 'image/png') + ';base64,' + base64;
160
+ }
161
+
162
+ /**
163
+ * Xóa toàn bộ attachments trong IndexedDB.
164
+ * @returns {Promise<void>}
165
+ */
166
+ async function clearAllAttachments() {
167
+ var db = await openChatHistoryDB();
168
+ return new Promise(function (resolve, reject) {
169
+ var tx = db.transaction(CHAT_ATTACHMENT_STORE_NAME, 'readwrite');
170
+ var store = tx.objectStore(CHAT_ATTACHMENT_STORE_NAME);
171
+ var request = store.clear();
172
+ request.onsuccess = function () { resolve(); };
173
+ request.onerror = function (e) { reject(e.target.error); };
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Lấy N message gần nhất từ IndexedDB.
179
+ * @param {number} count - Số lượng message cần lấy
180
+ * @returns {Promise<Array>} Mảng records sắp xếp theo timestamp tăng dần
181
+ */
182
+ async function getRecentMessages(count) {
183
+ var db = await openChatHistoryDB();
184
+ return new Promise(function (resolve, reject) {
185
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
186
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
187
+ var index = store.index('timestamp');
188
+ var results = [];
189
+ var request = index.openCursor(null, 'prev'); // Mới nhất trước
190
+ request.onsuccess = function (e) {
191
+ var cursor = e.target.result;
192
+ if (cursor && results.length < count) {
193
+ results.push(cursor.value);
194
+ cursor.continue();
195
+ } else {
196
+ resolve(results.reverse()); // Đảo lại: cũ → mới
197
+ }
198
+ };
199
+ request.onerror = function (e) { reject(e.target.error); };
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Lấy messages với paging.
205
+ * @param {number} page - Trang (bắt đầu từ 1)
206
+ * @param {number} pageSize - Số message mỗi trang
207
+ * @returns {Promise<{messages: Array, total: number, page: number, totalPages: number}>}
208
+ */
209
+ async function getMessagesPage(page, pageSize) {
210
+ var db = await openChatHistoryDB();
211
+ return new Promise(function (resolve, reject) {
212
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
213
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
214
+
215
+ var countReq = store.count();
216
+ countReq.onsuccess = function () {
217
+ var total = countReq.result;
218
+ var totalPages = Math.max(1, Math.ceil(total / pageSize));
219
+ var safePage = Math.min(Math.max(1, page), totalPages);
220
+
221
+ var index = store.index('timestamp');
222
+ var all = [];
223
+ var cursorReq = index.openCursor(null, 'prev');
224
+ cursorReq.onsuccess = function (e) {
225
+ var cursor = e.target.result;
226
+ if (cursor) {
227
+ all.push(cursor.value);
228
+ cursor.continue();
229
+ } else {
230
+ var start = (safePage - 1) * pageSize;
231
+ var slice = all.slice(start, start + pageSize);
232
+ resolve({
233
+ messages: slice,
234
+ total: total,
235
+ page: safePage,
236
+ totalPages: totalPages
237
+ });
238
+ }
239
+ };
240
+ cursorReq.onerror = function (e) { reject(e.target.error); };
241
+ };
242
+ countReq.onerror = function (e) { reject(e.target.error); };
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Xóa toàn bộ messages trong IndexedDB.
248
+ * @returns {Promise<void>}
249
+ */
250
+ async function clearAllChatMessages() {
251
+ var db = await openChatHistoryDB();
252
+ return new Promise(function (resolve, reject) {
253
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readwrite');
254
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
255
+ var request = store.clear();
256
+ request.onsuccess = function () { resolve(); };
257
+ request.onerror = function (e) { reject(e.target.error); };
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Đếm tổng số messages trong IndexedDB.
263
+ * @returns {Promise<number>}
264
+ */
265
+ async function countChatMessages() {
266
+ var db = await openChatHistoryDB();
267
+ return new Promise(function (resolve, reject) {
268
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
269
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
270
+ var request = store.count();
271
+ request.onsuccess = function () { resolve(request.result); };
272
+ request.onerror = function (e) { reject(e.target.error); };
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Đọc retention config từ localStorage.
278
+ * @returns {{mode: string, value: number}}
279
+ */
280
+ function getRetentionConfig() {
281
+ try {
282
+ var raw = localStorage.getItem('hikari_retention_config');
283
+ if (raw) {
284
+ var parsed = JSON.parse(raw);
285
+ if (parsed && parsed.mode && typeof parsed.value === 'number') {
286
+ return { mode: parsed.mode, value: parsed.value };
287
+ }
288
+ }
289
+ } catch (e) { /* ignore */ }
290
+ return { mode: 'count', value: 50 };
291
+ }
292
+
293
+ /**
294
+ * Lưu retention config vào localStorage và áp dụng ngay.
295
+ * @param {string} mode - 'count' hoặc 'days'
296
+ * @param {number} value - Giá trị tương ứng
297
+ * @returns {Promise<void>}
298
+ */
299
+ async function setRetentionConfig(mode, value) {
300
+ localStorage.setItem('hikari_retention_config', JSON.stringify({ mode: mode, value: value }));
301
+ await applyRetentionPolicy(mode, value);
302
+ }
303
+
304
+ /**
305
+ * Áp dụng retention policy để xóa messages cũ.
306
+ * - mode="count": giữ lại tối đa `value` messages mới nhất, xóa các messages cũ hơn
307
+ * - mode="days": xóa tất cả messages có timestamp < Date.now() - value * 86400000
308
+ * @param {string} mode - 'count' hoặc 'days'
309
+ * @param {number} value - Giá trị tương ứng
310
+ * @returns {Promise<void>}
311
+ */
312
+ async function applyRetentionPolicy(mode, value) {
313
+ var db = await openChatHistoryDB();
314
+ if (mode === 'count') {
315
+ // Lấy tất cả IDs theo thứ tự timestamp tăng dần (cũ nhất trước)
316
+ var allIds = await new Promise(function (resolve, reject) {
317
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readonly');
318
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
319
+ var index = store.index('timestamp');
320
+ var ids = [];
321
+ var request = index.openCursor(null, 'next'); // cũ nhất trước
322
+ request.onsuccess = function (e) {
323
+ var cursor = e.target.result;
324
+ if (cursor) {
325
+ ids.push(cursor.primaryKey);
326
+ cursor.continue();
327
+ } else {
328
+ resolve(ids);
329
+ }
330
+ };
331
+ request.onerror = function (e) { reject(e.target.error); };
332
+ });
333
+
334
+ var deleteCount = allIds.length - value;
335
+ if (deleteCount <= 0) return;
336
+
337
+ var idsToDelete = allIds.slice(0, deleteCount);
338
+ await new Promise(function (resolve, reject) {
339
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readwrite');
340
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
341
+ var pending = idsToDelete.length;
342
+ if (pending === 0) { resolve(); return; }
343
+ idsToDelete.forEach(function (id) {
344
+ var req = store.delete(id);
345
+ req.onsuccess = function () {
346
+ pending--;
347
+ if (pending === 0) resolve();
348
+ };
349
+ req.onerror = function (e) { reject(e.target.error); };
350
+ });
351
+ });
352
+ } else if (mode === 'days') {
353
+ var cutoff = Date.now() - value * 86400000;
354
+ await new Promise(function (resolve, reject) {
355
+ var tx = db.transaction(CHAT_HISTORY_STORE_NAME, 'readwrite');
356
+ var store = tx.objectStore(CHAT_HISTORY_STORE_NAME);
357
+ var index = store.index('timestamp');
358
+ // IDBKeyRange: timestamp < cutoff
359
+ var range = IDBKeyRange.upperBound(cutoff, true);
360
+ var request = index.openCursor(range);
361
+ request.onsuccess = function (e) {
362
+ var cursor = e.target.result;
363
+ if (cursor) {
364
+ cursor.delete();
365
+ cursor.continue();
366
+ } else {
367
+ resolve();
368
+ }
369
+ };
370
+ request.onerror = function (e) { reject(e.target.error); };
371
+ });
372
+ }
373
+ }
374
+
375
+ // Node/test: export to globalThis (stub functions khi IndexedDB không khả dụng)
376
+ if (typeof module !== 'undefined' && module.exports) {
377
+ globalThis.saveChatMessage = function () { return Promise.resolve({ messageId: 0, attachmentId: null }); };
378
+ globalThis.saveAttachment = function () { return Promise.resolve(0); };
379
+ globalThis.getAttachment = function () { return Promise.resolve(null); };
380
+ globalThis.getAttachmentByMessageId = function () { return Promise.resolve(null); };
381
+ globalThis.attachmentToDataURL = attachmentToDataURL;
382
+ globalThis.clearAllAttachments = function () { return Promise.resolve(); };
383
+ globalThis.getRecentMessages = function () { return Promise.resolve([]); };
384
+ globalThis.getMessagesPage = function () { return Promise.resolve({ messages: [], total: 0, page: 1, totalPages: 1 }); };
385
+ globalThis.clearAllChatMessages = function () { return Promise.resolve(); };
386
+ globalThis.countChatMessages = function () { return Promise.resolve(0); };
387
+ globalThis.getRetentionConfig = function () { return { mode: 'count', value: 50 }; };
388
+ globalThis.setRetentionConfig = function () { return Promise.resolve(); };
389
+ globalThis.applyRetentionPolicy = function () { return Promise.resolve(); };
390
+ }
data/help-content.json CHANGED
@@ -1,134 +1,134 @@
1
- {
2
- "vi": {
3
- "title": "📖 Hướng dẫn sử dụng",
4
- "sections": [
5
- {
6
- "icon": "💬",
7
- "heading": "Cách chat với Hikari",
8
- "items": [
9
- "Gõ tin nhắn vào ô bên dưới rồi nhấn <b>Gửi</b> hoặc phím <b>Enter</b>.",
10
- "Hikari hỗ trợ 3 ngôn ngữ: Tiếng Việt, English, 日本語. Chọn ngôn ngữ ở góc trên bên phải.",
11
- "Bạn có thể hỏi bất cứ điều gì — chào hỏi, kiến thức, tính toán, chuyển đổi đơn vị, xem giờ..."
12
- ]
13
- },
14
- {
15
- "icon": "🤖",
16
- "heading": "Cách Hikari phản hồi",
17
- "items": [
18
- "Hikari so khớp tin nhắn của bạn với các <b>trigger</b> đã được lập trình sẵn.",
19
- "Mỗi phản hồi kèm <b>Confidence</b> (độ tin cậy) cho biết mức độ khớp.",
20
- "<span class=\"help-dot green\"></span> <b>Xanh lá (≥50%)</b>: Hikari tự tin với câu trả lời.",
21
- "<span class=\"help-dot red\"></span> <b>Đỏ (&lt;50%)</b>: Hikari không chắc chắn, sẽ thử hỏi dịch vụ bổ sung."
22
- ]
23
- },
24
- {
25
- "icon": "⚡",
26
- "heading": "Các lệnh đặc biệt",
27
- "items": [
28
- "<span class=\"help-example\">tính 2 + 3</span> — Tính toán biểu thức toán học",
29
- "<span class=\"help-example\">đổi 5 km sang m</span> — Chuyển đổi đơn vị",
30
- "<span class=\"help-example\">mấy giờ</span> — Xem giờ hiện tại",
31
- "<span class=\"help-example\">hôm nay ngày mấy</span> — Xem ngày tháng",
32
- "<span class=\"help-example\">tìm kiếm ...</span> — Tìm câu trả lời phù hợp nhất"
33
- ]
34
- },
35
- {
36
- "icon": "📋",
37
- "heading": "Mẹo sử dụng",
38
- "items": [
39
- "Nhấn nút <b>📋</b> để xem danh sách tất cả câu hỏi Hikari có thể trả lời.",
40
- "Nhấn nút <b>🔧</b> để xem danh sách các chức năng mở rộng (Object Macros).",
41
- "Nếu Hikari không hiểu, hãy thử diễn đạt lại câu hỏi đơn giản hơn."
42
- ]
43
- }
44
- ]
45
- },
46
- "en": {
47
- "title": "📖 User Guide",
48
- "sections": [
49
- {
50
- "icon": "💬",
51
- "heading": "How to chat with Hikari",
52
- "items": [
53
- "Type your message in the input box below and press <b>Send</b> or hit <b>Enter</b>.",
54
- "Hikari supports 3 languages: Tiếng Việt, English, 日本語. Select from the dropdown in the top right.",
55
- "You can ask anything — greetings, knowledge, math, unit conversion, time..."
56
- ]
57
- },
58
- {
59
- "icon": "🤖",
60
- "heading": "How Hikari responds",
61
- "items": [
62
- "Hikari matches your message against pre-programmed <b>triggers</b>.",
63
- "Each response includes a <b>Confidence</b> score showing how well it matched.",
64
- "<span class=\"help-dot green\"></span> <b>Green (≥50%)</b>: Hikari is confident in the answer.",
65
- "<span class=\"help-dot red\"></span> <b>Red (&lt;50%)</b>: Hikari is unsure and will try a fallback service."
66
- ]
67
- },
68
- {
69
- "icon": "⚡",
70
- "heading": "Special commands",
71
- "items": [
72
- "<span class=\"help-example\">calculate 2 + 3</span> — Evaluate math expressions",
73
- "<span class=\"help-example\">convert 5 km to m</span> — Convert units",
74
- "<span class=\"help-example\">what time is it</span> — Check current time",
75
- "<span class=\"help-example\">what date is it</span> — Check today's date",
76
- "<span class=\"help-example\">search ...</span> — Find the best matching answer"
77
- ]
78
- },
79
- {
80
- "icon": "📋",
81
- "heading": "Tips",
82
- "items": [
83
- "Click <b>📋</b> to see all questions Hikari can answer.",
84
- "Click <b>🔧</b> to see available extended features (Object Macros).",
85
- "If Hikari doesn't understand, try rephrasing your question more simply."
86
- ]
87
- }
88
- ]
89
- },
90
- "ja": {
91
- "title": "📖 使い方ガイド",
92
- "sections": [
93
- {
94
- "icon": "💬",
95
- "heading": "ひかりとのチャット方法",
96
- "items": [
97
- "下の入力欄にメッセージを入力し、<b>送信</b>ボタンまたは<b>Enter</b>キーを押してください。",
98
- "ひかりは3つの言語に対応:Tiếng Việt、English、日本語。右上のドロップダウンで選択できます。",
99
- "何でも聞いてください — 挨拶、知識、計算、単位変換、時間確認..."
100
- ]
101
- },
102
- {
103
- "icon": "🤖",
104
- "heading": "ひかりの応答方法",
105
- "items": [
106
- "ひかりはメッセージをプログラムされた<b>トリガー</b>と照合します。",
107
- "各応答には一致度を示す<b>Confidence</b>(信頼度)スコアが付きます。",
108
- "<span class=\"help-dot green\"></span> <b>緑(≥50%)</b>:ひかりは回答に自信があります。",
109
- "<span class=\"help-dot red\"></span> <b>赤(&lt;50%)</b>:ひかりは不確かで、フォールバックサービスを試みます。"
110
- ]
111
- },
112
- {
113
- "icon": "⚡",
114
- "heading": "特別なコマンド",
115
- "items": [
116
- "<span class=\"help-example\">計算 2 + 3</span> — 数式を計算",
117
- "<span class=\"help-example\">変換 5 km を m</span> — 単位を変換",
118
- "<span class=\"help-example\">今何時</span> — 現在の時刻を確認",
119
- "<span class=\"help-example\">今日は何日</span> — 今日の日付を確認",
120
- "<span class=\"help-example\">検索 ...</span> — 最適な回答を検索"
121
- ]
122
- },
123
- {
124
- "icon": "📋",
125
- "heading": "ヒント",
126
- "items": [
127
- "<b>📋</b>ボタンでひかりが答えられる質問一覧を表示。",
128
- "<b>🔧</b>ボタンで拡張機能(Object Macros)一覧を表示。",
129
- "ひかりが理解できない場合は、より簡単な表現で質問してみてください。"
130
- ]
131
- }
132
- ]
133
- }
134
- }
 
1
+ {
2
+ "vi": {
3
+ "title": "📖 Hướng dẫn sử dụng",
4
+ "sections": [
5
+ {
6
+ "icon": "💬",
7
+ "heading": "Cách chat với Hikari",
8
+ "items": [
9
+ "Gõ tin nhắn vào ô bên dưới rồi nhấn <b>Gửi</b> hoặc phím <b>Enter</b>.",
10
+ "Hikari hỗ trợ 3 ngôn ngữ: Tiếng Việt, English, 日本語. Chọn ngôn ngữ ở góc trên bên phải.",
11
+ "Bạn có thể hỏi bất cứ điều gì — chào hỏi, kiến thức, tính toán, chuyển đổi đơn vị, xem giờ..."
12
+ ]
13
+ },
14
+ {
15
+ "icon": "🤖",
16
+ "heading": "Cách Hikari phản hồi",
17
+ "items": [
18
+ "Hikari so khớp tin nhắn của bạn với các <b>trigger</b> đã được lập trình sẵn.",
19
+ "Mỗi phản hồi kèm <b>Confidence</b> (độ tin cậy) cho biết mức độ khớp.",
20
+ "<span class=\"help-dot green\"></span> <b>Xanh lá (≥50%)</b>: Hikari tự tin với câu trả lời.",
21
+ "<span class=\"help-dot red\"></span> <b>Đỏ (&lt;50%)</b>: Hikari không chắc chắn, sẽ thử hỏi dịch vụ bổ sung."
22
+ ]
23
+ },
24
+ {
25
+ "icon": "⚡",
26
+ "heading": "Các lệnh đặc biệt",
27
+ "items": [
28
+ "<span class=\"help-example\">tính 2 + 3</span> — Tính toán biểu thức toán học",
29
+ "<span class=\"help-example\">đổi 5 km sang m</span> — Chuyển đổi đơn vị",
30
+ "<span class=\"help-example\">mấy giờ</span> — Xem giờ hiện tại",
31
+ "<span class=\"help-example\">hôm nay ngày mấy</span> — Xem ngày tháng",
32
+ "<span class=\"help-example\">tìm kiếm ...</span> — Tìm câu trả lời phù hợp nhất"
33
+ ]
34
+ },
35
+ {
36
+ "icon": "📋",
37
+ "heading": "Mẹo sử dụng",
38
+ "items": [
39
+ "Nhấn nút <b>📋</b> để xem danh sách tất cả câu hỏi Hikari có thể trả lời.",
40
+ "Nhấn nút <b>🔧</b> để xem danh sách các chức năng mở rộng (Object Macros).",
41
+ "Nếu Hikari không hiểu, hãy thử diễn đạt lại câu hỏi đơn giản hơn."
42
+ ]
43
+ }
44
+ ]
45
+ },
46
+ "en": {
47
+ "title": "📖 User Guide",
48
+ "sections": [
49
+ {
50
+ "icon": "💬",
51
+ "heading": "How to chat with Hikari",
52
+ "items": [
53
+ "Type your message in the input box below and press <b>Send</b> or hit <b>Enter</b>.",
54
+ "Hikari supports 3 languages: Tiếng Việt, English, 日本語. Select from the dropdown in the top right.",
55
+ "You can ask anything — greetings, knowledge, math, unit conversion, time..."
56
+ ]
57
+ },
58
+ {
59
+ "icon": "🤖",
60
+ "heading": "How Hikari responds",
61
+ "items": [
62
+ "Hikari matches your message against pre-programmed <b>triggers</b>.",
63
+ "Each response includes a <b>Confidence</b> score showing how well it matched.",
64
+ "<span class=\"help-dot green\"></span> <b>Green (≥50%)</b>: Hikari is confident in the answer.",
65
+ "<span class=\"help-dot red\"></span> <b>Red (&lt;50%)</b>: Hikari is unsure and will try a fallback service."
66
+ ]
67
+ },
68
+ {
69
+ "icon": "⚡",
70
+ "heading": "Special commands",
71
+ "items": [
72
+ "<span class=\"help-example\">calculate 2 + 3</span> — Evaluate math expressions",
73
+ "<span class=\"help-example\">convert 5 km to m</span> — Convert units",
74
+ "<span class=\"help-example\">what time is it</span> — Check current time",
75
+ "<span class=\"help-example\">what date is it</span> — Check today's date",
76
+ "<span class=\"help-example\">search ...</span> — Find the best matching answer"
77
+ ]
78
+ },
79
+ {
80
+ "icon": "📋",
81
+ "heading": "Tips",
82
+ "items": [
83
+ "Click <b>📋</b> to see all questions Hikari can answer.",
84
+ "Click <b>🔧</b> to see available extended features (Object Macros).",
85
+ "If Hikari doesn't understand, try rephrasing your question more simply."
86
+ ]
87
+ }
88
+ ]
89
+ },
90
+ "ja": {
91
+ "title": "📖 使い方ガイド",
92
+ "sections": [
93
+ {
94
+ "icon": "💬",
95
+ "heading": "ひかりとのチャット方法",
96
+ "items": [
97
+ "下の入力欄にメッセージを入力し、<b>送信</b>ボタンまたは<b>Enter</b>キーを押してください。",
98
+ "ひかりは3つの言語に対応:Tiếng Việt、English、日本語。右上のドロップダウンで選択できます。",
99
+ "何でも聞いてください — 挨拶、知識、計算、単位変換、時間確認..."
100
+ ]
101
+ },
102
+ {
103
+ "icon": "🤖",
104
+ "heading": "ひかりの応答方法",
105
+ "items": [
106
+ "ひかりはメッセージをプログラムされた<b>トリガー</b>と照合します。",
107
+ "各応答には一致度を示す<b>Confidence</b>(信頼度)スコアが付きます。",
108
+ "<span class=\"help-dot green\"></span> <b>緑(≥50%)</b>:ひかりは回答に自信があります。",
109
+ "<span class=\"help-dot red\"></span> <b>赤(&lt;50%)</b>:ひかりは不確かで、フォールバックサービスを試みます。"
110
+ ]
111
+ },
112
+ {
113
+ "icon": "⚡",
114
+ "heading": "特別なコマンド",
115
+ "items": [
116
+ "<span class=\"help-example\">計算 2 + 3</span> — 数式を計算",
117
+ "<span class=\"help-example\">変換 5 km を m</span> — 単位を変換",
118
+ "<span class=\"help-example\">今何時</span> — 現在の時刻を確認",
119
+ "<span class=\"help-example\">今日は何日</span> — 今日の日付を確認",
120
+ "<span class=\"help-example\">検索 ...</span> — 最適な回答を検索"
121
+ ]
122
+ },
123
+ {
124
+ "icon": "📋",
125
+ "heading": "ヒント",
126
+ "items": [
127
+ "<b>📋</b>ボタンでひかりが答えられる質問一覧を表示。",
128
+ "<b>🔧</b>ボタンで拡張機能(Object Macros)一覧を表示。",
129
+ "ひかりが理解できない場合は、より簡単な表現で質問してみてください。"
130
+ ]
131
+ }
132
+ ]
133
+ }
134
+ }
data/preprocessed.json CHANGED
The diff for this file is too large to render. See raw diff
 
data/qa-dataset.json CHANGED
@@ -1,44 +1,44 @@
1
- {
2
- "vi": [
3
- { "q": "hikari là gì", "a": "Hikari là chatbot thông minh hỗ trợ đa ngôn ngữ! 🌟" },
4
- { "q": "ai tạo ra bạn", "a": "Mình được tạo ra bởi một nhà phát triển đam mê chatbot!" },
5
- { "q": "bạn có thể giúp gì", "a": "Mình có thể trò chuyện, tính toán, chuyển đổi đơn vị, xem giờ và tìm câu trả lời cho bạn! 🌟" },
6
- { "q": "bạn biết nói mấy ngôn ngữ", "a": "Mình biết nói 3 ngôn ngữ: tiếng Việt, tiếng Anh và tiếng Nhật! 🌍" },
7
- { "q": "hikari nghĩa là gì", "a": "Hikari (ひかり) có nghĩa là \"ánh sáng\" trong tiếng Nhật! ✨" },
8
- { "q": "bạn bao nhiêu tuổi", "a": "Mình là chatbot nên không có tuổi, nhưng mình luôn trẻ trung! 😄" },
9
- { "q": "bạn thích gì", "a": "Mình thích trò chuyện và giúp đỡ mọi người! 💬" },
10
- { "q": "cách sử dụng chatbot", "a": "Bạn chỉ cần gõ tin nhắn và nhấn Gửi. Mình sẽ trả lời ngay! 📝" },
11
- { "q": "chatbot là gì", "a": "Chatbot là chương trình máy tính có thể trò chuyện với con người! 🤖" },
12
- { "q": "rivescript là gì", "a": "RiveScript là thư viện xử lý hội thoại mà mình sử dụng làm engine! ⚙️" },
13
- { "q": "bạn có thông minh không", "a": "Mình cố gắng thông minh nhất có thể để giúp bạn! 🧠" },
14
- { "q": "làm sao để đổi ngôn ngữ", "a": "Bạn có thể chọn ngôn ngữ từ menu dropdown ở góc trên bên phải! 🌐" }
15
- ],
16
- "en": [
17
- { "q": "what is hikari", "a": "Hikari is a smart multilingual chatbot! 🌟" },
18
- { "q": "who created you", "a": "I was created by a developer passionate about chatbots!" },
19
- { "q": "what can you help with", "a": "I can chat, calculate, convert units, tell time, and find answers for you! 🌟" },
20
- { "q": "how many languages do you know", "a": "I know 3 languages: Vietnamese, English, and Japanese! 🌍" },
21
- { "q": "what does hikari mean", "a": "Hikari (ひかり) means \"light\" in Japanese! ✨" },
22
- { "q": "how old are you", "a": "I'm a chatbot so I don't have an age, but I'm always young at heart! 😄" },
23
- { "q": "what do you like", "a": "I love chatting and helping people! 💬" },
24
- { "q": "how to use this chatbot", "a": "Just type your message and press Send. I will reply right away! 📝" },
25
- { "q": "what is a chatbot", "a": "A chatbot is a computer program that can have conversations with humans! 🤖" },
26
- { "q": "what is rivescript", "a": "RiveScript is the conversation engine that powers me! ⚙️" },
27
- { "q": "are you smart", "a": "I try my best to be as helpful as possible! 🧠" },
28
- { "q": "how to change language", "a": "You can select a language from the dropdown menu in the top right corner! 🌐" }
29
- ],
30
- "ja": [
31
- { "q": "ひかりとは何ですか", "a": "ひかりは多言語対応のスマートチャットボットです!🌟" },
32
- { "q": "誰が作りましたか", "a": "チャットボットに情熱を持つ開発者が作りました!" },
33
- { "q": "何を手伝ってくれますか", "a": "チャット、計算、単位変換、時間確認、回答検索ができます!🌟" },
34
- { "q": "いくつの言語を知っていますか", "a": "ベトナム語、英語、日本語の3つの言語を知っています!🌍" },
35
- { "q": "ひかりの意味は何ですか", "a": "ひかり(光)は日本語で「light」という意味です!✨" },
36
- { "q": "何歳ですか", "a": "チャットボットなので年齢はありませんが、いつも若々しいです!😄" },
37
- { "q": "何が好きですか", "a": "チャットして人々を助けることが好きです!💬" },
38
- { "q": "チャットボットの使い方", "a": "メッセージを入力して送信ボタンを押すだけです。すぐに返信します!📝" },
39
- { "q": "チャットボットとは何ですか", "a": "チャットボットは人間と会話できるコンピュータプログラムです!🤖" },
40
- { "q": "rivescriptとは何ですか", "a": "RiveScriptは私を動かす会話エンジンです!⚙️" },
41
- { "q": "賢いですか", "a": "できる限りお役に立てるよう頑張っています!🧠" },
42
- { "q": "言語を変更する方法", "a": "右上のドロップダウンメニューから言語を選択できます!🌐" }
43
- ]
44
- }
 
1
+ {
2
+ "vi": [
3
+ { "q": "hikari là gì", "a": "Hikari là chatbot thông minh hỗ trợ đa ngôn ngữ! 🌟" },
4
+ { "q": "ai tạo ra bạn", "a": "Mình được tạo ra bởi một nhà phát triển đam mê chatbot!" },
5
+ { "q": "bạn có thể giúp gì", "a": "Mình có thể trò chuyện, tính toán, chuyển đổi đơn vị, xem giờ và tìm câu trả lời cho bạn! 🌟" },
6
+ { "q": "bạn biết nói mấy ngôn ngữ", "a": "Mình biết nói 3 ngôn ngữ: tiếng Việt, tiếng Anh và tiếng Nhật! 🌍" },
7
+ { "q": "hikari nghĩa là gì", "a": "Hikari (ひかり) có nghĩa là \"ánh sáng\" trong tiếng Nhật! ✨" },
8
+ { "q": "bạn bao nhiêu tuổi", "a": "Mình là chatbot nên không có tuổi, nhưng mình luôn trẻ trung! 😄" },
9
+ { "q": "bạn thích gì", "a": "Mình thích trò chuyện và giúp đỡ mọi người! 💬" },
10
+ { "q": "cách sử dụng chatbot", "a": "Bạn chỉ cần gõ tin nhắn và nhấn Gửi. Mình sẽ trả lời ngay! 📝" },
11
+ { "q": "chatbot là gì", "a": "Chatbot là chương trình máy tính có thể trò chuyện với con người! 🤖" },
12
+ { "q": "rivescript là gì", "a": "RiveScript là thư viện xử lý hội thoại mà mình sử dụng làm engine! ⚙️" },
13
+ { "q": "bạn có thông minh không", "a": "Mình cố gắng thông minh nhất có thể để giúp bạn! 🧠" },
14
+ { "q": "làm sao để đổi ngôn ngữ", "a": "Bạn có thể chọn ngôn ngữ từ menu dropdown ở góc trên bên phải! 🌐" }
15
+ ],
16
+ "en": [
17
+ { "q": "what is hikari", "a": "Hikari is a smart multilingual chatbot! 🌟" },
18
+ { "q": "who created you", "a": "I was created by a developer passionate about chatbots!" },
19
+ { "q": "what can you help with", "a": "I can chat, calculate, convert units, tell time, and find answers for you! 🌟" },
20
+ { "q": "how many languages do you know", "a": "I know 3 languages: Vietnamese, English, and Japanese! 🌍" },
21
+ { "q": "what does hikari mean", "a": "Hikari (ひかり) means \"light\" in Japanese! ✨" },
22
+ { "q": "how old are you", "a": "I'm a chatbot so I don't have an age, but I'm always young at heart! 😄" },
23
+ { "q": "what do you like", "a": "I love chatting and helping people! 💬" },
24
+ { "q": "how to use this chatbot", "a": "Just type your message and press Send. I will reply right away! 📝" },
25
+ { "q": "what is a chatbot", "a": "A chatbot is a computer program that can have conversations with humans! 🤖" },
26
+ { "q": "what is rivescript", "a": "RiveScript is the conversation engine that powers me! ⚙️" },
27
+ { "q": "are you smart", "a": "I try my best to be as helpful as possible! 🧠" },
28
+ { "q": "how to change language", "a": "You can select a language from the dropdown menu in the top right corner! 🌐" }
29
+ ],
30
+ "ja": [
31
+ { "q": "ひかりとは何ですか", "a": "ひかりは多言語対応のスマートチャットボットです!🌟" },
32
+ { "q": "誰が作りましたか", "a": "チャットボットに情熱を持つ開発者が作りました!" },
33
+ { "q": "何を手伝ってくれますか", "a": "チャット、計算、単位変換、時間確認、回答検索ができます!🌟" },
34
+ { "q": "いくつの言語を知っていますか", "a": "ベトナム語、英語、日本語の3つの言語を知っています!🌍" },
35
+ { "q": "ひかりの意味は何ですか", "a": "ひかり(光)は日本語で「light」という意味です!✨" },
36
+ { "q": "何歳ですか", "a": "チャットボットなので年齢はありませんが、いつも若々しいです!😄" },
37
+ { "q": "何が好きですか", "a": "チャットして人々を助けることが好きです!💬" },
38
+ { "q": "チャットボットの使い方", "a": "メッセージを入力して送信ボタンを押すだけです。すぐに返信します!📝" },
39
+ { "q": "チャットボットとは何ですか", "a": "チャットボットは人間と会話できるコンピュータプログラムです!🤖" },
40
+ { "q": "rivescriptとは何ですか", "a": "RiveScriptは私を動かす会話エンジンです!⚙️" },
41
+ { "q": "賢いですか", "a": "できる限りお役に立てるよう頑張っています!🧠" },
42
+ { "q": "言語を変更する方法", "a": "右上のドロップダウンメニューから言語を選択できます!🌐" }
43
+ ]
44
+ }
data/specific-responses.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "vi": {
3
- "hikari là ai": "Mình là Hikari, chatbot hỗ trợ đa ngôn ngữ! 🌟",
4
- "phiên bản": "Hikari phiên bản 1.0 — sử dụng RiveScript engine.",
5
- "bạn nói được mấy ngôn ngữ": "Mình hỗ trợ 3 ngôn ngữ: tiếng Việt, tiếng Anh và tiếng Nhật! 🌍",
6
- "ai tạo ra hikari": "Hikari được tạo bởi một nhà phát triển đam mê chatbot và AI! 🚀",
7
- "hikari có nghĩa là gì": "Hikari (ひかり) có nghĩa là \"ánh sáng\" trong tiếng Nhật! ✨"
8
- },
9
- "en": {
10
- "who is hikari": "I am Hikari, a multilingual chatbot! 🌟",
11
- "version": "Hikari version 1.0 — powered by RiveScript engine.",
12
- "how many languages do you speak": "I support 3 languages: Vietnamese, English, and Japanese! 🌍",
13
- "who created hikari": "Hikari was created by a developer passionate about chatbots and AI! 🚀",
14
- "what does hikari mean": "Hikari (ひかり) means \"light\" in Japanese! ✨"
15
- },
16
- "ja": {
17
- "ひかりは誰": "私はひかり、多言語チャットボットです!🌟",
18
- "バージョン": "ひかりバージョン1.0 — RiveScriptエンジン搭載。",
19
- "いくつの言語を話せますか": "3つの言語に対応しています:ベトナム語、英語、日本語!🌍",
20
- "ひかりを作ったのは誰": "ひかりはチャットボットとAIに情熱を持つ開発者が作りました!🚀",
21
- "ひかりの意味は何": "ひかり(光)は日本語で「light」という意味です!✨"
22
- }
23
- }
 
1
+ {
2
+ "vi": {
3
+ "hikari là ai": "Mình là Hikari, chatbot hỗ trợ đa ngôn ngữ! 🌟",
4
+ "phiên bản": "Hikari phiên bản 1.0 — sử dụng RiveScript engine.",
5
+ "bạn nói được mấy ngôn ngữ": "Mình hỗ trợ 3 ngôn ngữ: tiếng Việt, tiếng Anh và tiếng Nhật! 🌍",
6
+ "ai tạo ra hikari": "Hikari được tạo bởi một nhà phát triển đam mê chatbot và AI! 🚀",
7
+ "hikari có nghĩa là gì": "Hikari (ひかり) có nghĩa là \"ánh sáng\" trong tiếng Nhật! ✨"
8
+ },
9
+ "en": {
10
+ "who is hikari": "I am Hikari, a multilingual chatbot! 🌟",
11
+ "version": "Hikari version 1.0 — powered by RiveScript engine.",
12
+ "how many languages do you speak": "I support 3 languages: Vietnamese, English, and Japanese! 🌍",
13
+ "who created hikari": "Hikari was created by a developer passionate about chatbots and AI! 🚀",
14
+ "what does hikari mean": "Hikari (ひかり) means \"light\" in Japanese! ✨"
15
+ },
16
+ "ja": {
17
+ "ひかりは誰": "私はひかり、多言語チャットボットです!🌟",
18
+ "バージョン": "ひかりバージョン1.0 — RiveScriptエンジン搭載。",
19
+ "いくつの言語を話せますか": "3つの言語に対応しています:ベトナム語、英語、日本語!🌍",
20
+ "ひかりを作ったのは誰": "ひかりはチャットボットとAIに情熱を持つ開発者が作りました!🚀",
21
+ "ひかりの意味は何": "ひかり(光)は日本語で「light」という意味です!✨"
22
+ }
23
+ }
index.html CHANGED
@@ -1,133 +1,186 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Hikari Chatbot</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌟</text></svg>">
8
- <link rel="stylesheet" href="style.css">
9
- </head>
10
- <body>
11
- <div class="chat-container">
12
- <header class="chat-header">
13
- <h1>Hikari</h1>
14
- <div class="header-controls">
15
- <label class="thinking-toggle" title="Enable/Disable LLM Thinking">
16
- <input type="checkbox" id="thinking-toggle" onchange="if(typeof setLLMThinkingEnabled==='function')setLLMThinkingEnabled(this.checked)">
17
- <span class="thinking-toggle-label">🧠</span>
18
- </label>
19
- <button id="help-button" title="Hướng dẫn sử dụng">❓</button>
20
- <button id="rules-button" title="Xem danh sách rules">📋</button>
21
- <button id="macros-button" title="Xem danh sách Object Macros">🔧</button>
22
- <button id="settings-button" title="Cài đặt">⚙️</button>
23
- <select id="language-selector">
24
- <option value="vi" selected>Tiếng Việt</option>
25
- <option value="en">English</option>
26
- <option value="ja">日本語</option>
27
- </select>
28
- </div>
29
- </header>
30
- <div id="rules-panel" class="slide-panel hidden">
31
- <div class="slide-panel-header">
32
- <h3 id="rules-title">📋 Các câu hỏi Hikari có thể trả lời</h3>
33
- <button class="slide-panel-close" onclick="toggleRulesPanel()" aria-label="Đóng">&times;</button>
34
- </div>
35
- <div class="slide-panel-body">
36
- <ul id="rules-list" class="rules-list"></ul>
37
- </div>
38
- </div>
39
- <div id="macros-panel" class="slide-panel hidden">
40
- <div class="slide-panel-header">
41
- <h3 id="macros-title">🔧 Object Macros (Subroutines)</h3>
42
- <button class="slide-panel-close" onclick="toggleMacrosPanel()" aria-label="Đóng">&times;</button>
43
- </div>
44
- <div class="slide-panel-body">
45
- <ul id="macros-list" class="macros-list"></ul>
46
- </div>
47
- </div>
48
- <div id="settings-panel" class="slide-panel hidden">
49
- <div class="slide-panel-header">
50
- <h3>⚙️ Settings</h3>
51
- <button class="slide-panel-close" onclick="toggleSettingsPanel()" aria-label="Đóng">&times;</button>
52
- </div>
53
- <div class="slide-panel-body settings-body">
54
- <div class="setting-item">
55
- <label>
56
- <input type="checkbox" id="history-toggle">
57
- Chat History (gửi lịch sử cho LLM)
58
- </label>
59
- </div>
60
- <div class="setting-item">
61
- <label>
62
- Max turns:
63
- <input type="number" id="history-max-turns" value="5" min="0" max="50" class="setting-number">
64
- </label>
65
- <span class="setting-hint">0 = không giới hạn</span>
66
- </div>
67
- <div class="setting-item setting-buttons">
68
- <button id="view-history-button" class="setting-btn">📜 View History</button>
69
- <button id="clear-history-button" class="setting-btn setting-btn-danger">🗑 Clear History</button>
70
- </div>
71
- </div>
72
- </div>
73
- <div id="message-display" class="message-display"></div>
74
- <div class="input-area">
75
- <button id="attach-button" title="Đính kèm ảnh" aria-label="Đính kèm ảnh">📎</button>
76
- <input type="file" id="file-input" accept="image/*" class="hidden">
77
- <input type="text" id="message-input" placeholder="Nhập tin nhắn...">
78
- <button id="send-button">Gửi</button>
79
- </div>
80
- <div id="attachment-preview" class="attachment-preview hidden">
81
- <img id="attachment-thumb" alt="Preview" class="attachment-thumb">
82
- <span id="attachment-name" class="attachment-name"></span>
83
- <button id="attachment-remove" class="attachment-remove" aria-label="Xóa">&times;</button>
84
- </div>
85
- </div>
86
- <!-- Help Dialog -->
87
- <div id="help-overlay" class="help-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="help-dialog-title">
88
- <div class="help-dialog">
89
- <div class="help-dialog-header">
90
- <h2 id="help-dialog-title">📖 Hướng dẫn sử dụng</h2>
91
- <button id="help-close-button" class="help-close-button" aria-label="Đóng">&times;</button>
92
- </div>
93
- <div id="help-dialog-body" class="help-dialog-body">
94
- <!-- Nội dung được render bởi JS theo ngôn ngữ -->
95
- </div>
96
- </div>
97
- </div>
98
-
99
- <!-- History Dialog -->
100
- <div id="history-overlay" class="help-overlay hidden" role="dialog" aria-modal="true">
101
- <div class="help-dialog history-dialog">
102
- <div class="help-dialog-header">
103
- <h2 id="history-dialog-title">📜 Chat History</h2>
104
- <div class="history-tab-buttons">
105
- <button id="history-tab-db" class="history-tab active">IndexedDB</button>
106
- <button id="history-tab-session" class="history-tab">Session</button>
107
- </div>
108
- <button id="history-close-button" class="help-close-button" aria-label="Đóng">&times;</button>
109
- </div>
110
- <div id="history-dialog-body" class="help-dialog-body">
111
- <div id="history-list" class="history-list"></div>
112
- <div id="history-paging" class="history-paging"></div>
113
- </div>
114
- </div>
115
- </div>
116
-
117
- <script src="https://unpkg.com/rivescript@latest/dist/rivescript.min.js"></script>
118
- <script src="brain.js"></script>
119
- <script src="data-loader.js"></script>
120
- <script src="data/chat-history-db.js"></script>
121
- <script src="adapters/text-similarity.js"></script>
122
- <script src="adapters/specific-response.js"></script>
123
- <script src="adapters/time-adapter.js"></script>
124
- <script src="adapters/math-adapter.js"></script>
125
- <script src="adapters/unit-conversion.js"></script>
126
- <script src="adapters/best-match.js"></script>
127
- <script src="adapters/logic-dispatcher.js"></script>
128
- <script src="adapters/web-search.js"></script>
129
- <script src="adapters/llm-adapter.js"></script>
130
- <script src="adapters/adapter-registry.js"></script>
131
- <script src="app.js"></script>
132
- </body>
133
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Hikari Chatbot</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌟</text></svg>">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div class="chat-container">
12
+ <header class="chat-header">
13
+ <h1>Hikari</h1>
14
+ <div class="header-controls">
15
+ <span id="interaction-mode-badge" class="interaction-mode-badge" title="Click để đổi chế độ tương tác">📝→📝</span>
16
+ <label class="thinking-toggle" title="Enable/Disable LLM Thinking">
17
+ <input type="checkbox" id="thinking-toggle" onchange="if(typeof setLLMThinkingEnabled==='function')setLLMThinkingEnabled(this.checked)">
18
+ <span class="thinking-toggle-label">🧠</span>
19
+ </label>
20
+ <button id="help-button" title="Hướng dẫn sử dụng"></button>
21
+ <button id="rules-button" title="Xem danh sách rules">📋</button>
22
+ <button id="macros-button" title="Xem danh sách Object Macros">🔧</button>
23
+ <button id="settings-button" title="Cài đặt">⚙️</button>
24
+ <select id="language-selector">
25
+ <option value="vi" selected>Tiếng Việt</option>
26
+ <option value="en">English</option>
27
+ <option value="ja">日本語</option>
28
+ </select>
29
+ </div>
30
+ </header>
31
+ <div id="rules-panel" class="slide-panel hidden">
32
+ <div class="slide-panel-header">
33
+ <h3 id="rules-title">📋 Các câu hỏi Hikari có thể trả lời</h3>
34
+ <button class="slide-panel-close" onclick="toggleRulesPanel()" aria-label="Đóng">&times;</button>
35
+ </div>
36
+ <div class="slide-panel-body">
37
+ <ul id="rules-list" class="rules-list"></ul>
38
+ </div>
39
+ </div>
40
+ <div id="macros-panel" class="slide-panel hidden">
41
+ <div class="slide-panel-header">
42
+ <h3 id="macros-title">🔧 Object Macros (Subroutines)</h3>
43
+ <button class="slide-panel-close" onclick="toggleMacrosPanel()" aria-label="Đóng">&times;</button>
44
+ </div>
45
+ <div class="slide-panel-body">
46
+ <ul id="macros-list" class="macros-list"></ul>
47
+ </div>
48
+ </div>
49
+ <div id="settings-panel" class="slide-panel hidden">
50
+ <div class="slide-panel-header">
51
+ <h3>⚙️ Settings</h3>
52
+ <button class="slide-panel-close" onclick="toggleSettingsPanel()" aria-label="Đóng">&times;</button>
53
+ </div>
54
+ <div class="slide-panel-body settings-body">
55
+ <div class="setting-item">
56
+ <label>
57
+ <input type="checkbox" id="history-toggle">
58
+ Chat History (gửi lịch sử cho LLM)
59
+ </label>
60
+ </div>
61
+ <div class="setting-item">
62
+ <label>
63
+ Max turns:
64
+ <input type="number" id="history-max-turns" value="5" min="0" max="50" class="setting-number">
65
+ </label>
66
+ <span class="setting-hint">0 = không giới hạn</span>
67
+ </div>
68
+ <div class="setting-item setting-buttons">
69
+ <button id="view-history-button" class="setting-btn">📜 View History</button>
70
+ <button id="clear-history-button" class="setting-btn setting-btn-danger">🗑 Clear History</button>
71
+ </div>
72
+ <div class="setting-item">
73
+ <label class="setting-label">Giới hạn lưu trữ:</label>
74
+ <select id="retention-mode-select" class="setting-select">
75
+ <option value="count">Giới hạn số lượng</option>
76
+ <option value="days">Giới hạn thời gian</option>
77
+ </select>
78
+ </div>
79
+ <div id="retention-count-section" class="setting-item">
80
+ <label>
81
+ Tối đa:
82
+ <input type="number" id="retention-max-count" value="50" min="1" max="10000" class="setting-number">
83
+ </label>
84
+ <span class="setting-hint">hội thoại</span>
85
+ </div>
86
+ <div id="retention-days-section" class="setting-item hidden">
87
+ <label>
88
+ Tối đa:
89
+ <input type="number" id="retention-max-days" value="30" min="1" max="3650" class="setting-number">
90
+ </label>
91
+ <span class="setting-hint">ngày</span>
92
+ </div>
93
+ <hr class="setting-divider">
94
+ <div class="setting-item">
95
+ <label class="setting-label">Chế độ tương tác:</label>
96
+ <select id="interaction-mode-select" class="setting-select">
97
+ <option value="text-text">📝→📝 Text → Text</option>
98
+ <option value="text-voice">📝→🔊 Text → Voice</option>
99
+ <option value="voice-text">🎤→📝 Voice Text</option>
100
+ <option value="voice-voice">🎤→🔊 Voice Voice</option>
101
+ </select>
102
+ </div>
103
+ <div id="tts-settings-section">
104
+ <div class="setting-item">
105
+ <label>
106
+ <input type="checkbox" id="voice-input-toggle">
107
+ Voice Input (Speech to Text)
108
+ </label>
109
+ </div>
110
+ <div class="setting-item">
111
+ <label>
112
+ <input type="checkbox" id="voice-output-toggle">
113
+ Voice Output (Text to Speech)
114
+ </label>
115
+ </div>
116
+ <div class="setting-item">
117
+ <label class="setting-label">Giọng đọc:</label>
118
+ <select id="tts-voice-select" class="setting-select"></select>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ <div id="message-display" class="message-display"></div>
124
+ <div class="input-area">
125
+ <button id="attach-button" title="Đính kèm ảnh" aria-label="Đính kèm ảnh">📎</button>
126
+ <input type="file" id="file-input" accept="image/*" class="hidden">
127
+ <button id="voice-input-button" class="hidden" title="Nhập bằng giọng nói" aria-label="Nhập bằng giọng nói">🎤</button>
128
+ <div id="adapter-prefix-badge" class="adapter-prefix-badge hidden" aria-live="polite"></div>
129
+ <input type="text" id="message-input" placeholder="Nhập tin nhắn...">
130
+ <button id="send-button">Gửi</button>
131
+ </div>
132
+ <div id="attachment-preview" class="attachment-preview hidden">
133
+ <img id="attachment-thumb" alt="Preview" class="attachment-thumb">
134
+ <span id="attachment-name" class="attachment-name"></span>
135
+ <button id="attachment-remove" class="attachment-remove" aria-label="Xóa">&times;</button>
136
+ </div>
137
+ </div>
138
+ <!-- Help Dialog -->
139
+ <div id="help-overlay" class="help-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="help-dialog-title">
140
+ <div class="help-dialog">
141
+ <div class="help-dialog-header">
142
+ <h2 id="help-dialog-title">📖 Hướng dẫn sử dụng</h2>
143
+ <button id="help-close-button" class="help-close-button" aria-label="Đóng">&times;</button>
144
+ </div>
145
+ <div id="help-dialog-body" class="help-dialog-body">
146
+ <!-- Nội dung được render bởi JS theo ngôn ngữ -->
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- History Dialog -->
152
+ <div id="history-overlay" class="help-overlay hidden" role="dialog" aria-modal="true">
153
+ <div class="help-dialog history-dialog">
154
+ <div class="help-dialog-header">
155
+ <h2 id="history-dialog-title">📜 Chat History</h2>
156
+ <div class="history-tab-buttons">
157
+ <button id="history-tab-db" class="history-tab active">IndexedDB</button>
158
+ <button id="history-tab-session" class="history-tab">Session</button>
159
+ </div>
160
+ <button id="history-close-button" class="help-close-button" aria-label="Đóng">&times;</button>
161
+ </div>
162
+ <div id="history-dialog-body" class="help-dialog-body">
163
+ <div id="history-list" class="history-list"></div>
164
+ <div id="history-paging" class="history-paging"></div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <script src="https://unpkg.com/rivescript@latest/dist/rivescript.min.js"></script>
170
+ <script src="brain.js"></script>
171
+ <script src="data-loader.js"></script>
172
+ <script src="data/chat-history-db.js"></script>
173
+ <script src="adapters/text-similarity.js"></script>
174
+ <script src="adapters/specific-response.js"></script>
175
+ <script src="adapters/time-adapter.js"></script>
176
+ <script src="adapters/math-adapter.js"></script>
177
+ <script src="adapters/unit-conversion.js"></script>
178
+ <script src="adapters/best-match.js"></script>
179
+ <script src="adapters/logic-dispatcher.js"></script>
180
+ <script src="adapters/web-search.js"></script>
181
+ <script src="adapters/llm-adapter.js"></script>
182
+ <script src="adapters/voice-adapter.js"></script>
183
+ <script src="adapters/adapter-registry.js"></script>
184
+ <script src="app.js"></script>
185
+ </body>
186
+ </html>
scripts/preprocess.js CHANGED
@@ -1,280 +1,280 @@
1
- #!/usr/bin/env node
2
- // ============================================================
3
- // scripts/preprocess.js — Tiền xử lý dữ liệu cho text similarity
4
- //
5
- // Chạy: node scripts/preprocess.js
6
- // Output: data/preprocessed.json
7
- //
8
- // Script này đọc QA dataset + specific responses + brain .rive files, thực hiện:
9
- // 1. Tokenize (tách từ)
10
- // 2. Normalize (lowercase, bỏ dấu tiếng Việt, bỏ punctuation)
11
- // 3. Tính TF vector cho mỗi câu hỏi
12
- // 4. Tính synonym group indices cho mỗi từ
13
- // 5. Tạo IDF (inverse document frequency) cho toàn bộ corpus
14
- // 6. Tính TF-IDF vector cho mỗi câu hỏi
15
- // 7. Lưu tất cả vào data/preprocessed.json
16
- //
17
- // Khi dữ liệu thay đổi (qa-dataset.json, specific-responses.json, brain/*.rive),
18
- // chạy lại script này để tạo preprocessed.json mới.
19
- // ============================================================
20
-
21
- var fs = require('fs');
22
- var path = require('path');
23
-
24
- var ROOT = path.join(__dirname, '..');
25
- var QA_DATASET = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'qa-dataset.json'), 'utf8'));
26
- var SPECIFIC_RESPONSES = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'specific-responses.json'), 'utf8'));
27
-
28
- // --- Parse brain .rive files to extract trigger-response pairs ---
29
- var BRAIN_DIR = path.join(ROOT, 'brain');
30
-
31
- /**
32
- * Parse file .rive và trích xuất các cặp trigger-response.
33
- * Bỏ qua: trigger wildcard mặc định (*), trigger chứa <call>, trigger chỉ có wildcard.
34
- */
35
- function parseBrainFile(filePath) {
36
- var content = fs.readFileSync(filePath, 'utf8');
37
- var lines = content.split('\n');
38
- var pairs = [];
39
- var currentTrigger = null;
40
-
41
- for (var i = 0; i < lines.length; i++) {
42
- var line = lines[i].trim();
43
-
44
- // Skip comments and empty lines
45
- if (line.indexOf('//') === 0 || line.length === 0) continue;
46
- // Skip directives (! version, ! var)
47
- if (line.indexOf('!') === 0) continue;
48
-
49
- if (line.indexOf('+ ') === 0) {
50
- currentTrigger = line.substring(2).trim();
51
- } else if (line.indexOf('- ') === 0 && currentTrigger) {
52
- var response = line.substring(2).trim();
53
-
54
- // Bỏ qua trigger wildcard mặc định
55
- if (currentTrigger === '*') { currentTrigger = null; continue; }
56
- // Bỏ qua response chứa <call> (adapter calls, không phải text response)
57
- if (response.indexOf('<call>') !== -1) { currentTrigger = null; continue; }
58
- // Bỏ qua trigger chỉ chứa wildcard (ví dụ: "* la ai")
59
- var nonWild = currentTrigger.replace(/\*/g, '').trim();
60
- if (nonWild.length < 2) { currentTrigger = null; continue; }
61
-
62
- pairs.push({ trigger: currentTrigger, response: response });
63
- currentTrigger = null;
64
- }
65
- }
66
- return pairs;
67
- }
68
-
69
- // --- Vietnamese diacritics removal (copy from app.js) ---
70
- var VIETNAMESE_DIACRITICS_MAP = {
71
- 'à':'a','á':'a','ả':'a','ã':'a','ạ':'a',
72
- 'ă':'a','ằ':'a','ắ':'a','ẳ':'a','ẵ':'a','ặ':'a',
73
- 'â':'a','ầ':'a','ấ':'a','ẩ':'a','ẫ':'a','ậ':'a',
74
- 'đ':'d',
75
- 'è':'e','é':'e','ẻ':'e','ẽ':'e','ẹ':'e',
76
- 'ê':'e','ề':'e','ế':'e','ể':'e','ễ':'e','ệ':'e',
77
- 'ì':'i','í':'i','ỉ':'i','ĩ':'i','ị':'i',
78
- 'ò':'o','ó':'o','ỏ':'o','õ':'o','ọ':'o',
79
- 'ô':'o','ồ':'o','ố':'o','ổ':'o','ỗ':'o','ộ':'o',
80
- 'ơ':'o','ờ':'o','ớ':'o','ở':'o','ỡ':'o','ợ':'o',
81
- 'ù':'u','ú':'u','ủ':'u','ũ':'u','ụ':'u',
82
- 'ư':'u','ừ':'u','ứ':'u','ử':'u','ữ':'u','ự':'u',
83
- 'ỳ':'y','ý':'y','ỷ':'y','ỹ':'y','ỵ':'y'
84
- };
85
-
86
- function removeDiacritics(str) {
87
- var result = '';
88
- for (var i = 0; i < str.length; i++) {
89
- var ch = str[i];
90
- result += VIETNAMESE_DIACRITICS_MAP[ch] || ch;
91
- }
92
- return result;
93
- }
94
-
95
- // --- Synonym groups (copy from text-similarity.js) ---
96
- var SYNONYM_GROUPS = [
97
- ['xin chao','chao','hi','hello','hey','yo'],
98
- ['tam biet','bye','goodbye','hen gap lai','tot lanh'],
99
- ['cam on','thanks','thank','thank you'],
100
- ['ten','name','ai','who','la ai'],
101
- ['lam gi','lam duoc','co the','giup','help','what can'],
102
- ['may gio','gio','time','clock','bao gio'],
103
- ['ngay','hom nay','date','today','ngay may'],
104
- ['thu','thu may','day','what day'],
105
- ['tinh','calculate','math','cong','tru','nhan','chia','plus','minus'],
106
- ['doi','convert','sang','to','chuyen doi'],
107
- ['chatbot','bot','ai','robot','may','machine'],
108
- ['la gi','what is','what','gi','mean'],
109
- ['khoe','vui','happy','fine','good','ok'],
110
- ['tuoi','age','old','bao nhieu tuoi'],
111
- ['o dau','where','dau','location'],
112
- ['thich','like','love','yeu'],
113
- ['huong dan','cach','how','guide','help','su dung','use']
114
- ];
115
-
116
- var synonymLookup = {};
117
- for (var g = 0; g < SYNONYM_GROUPS.length; g++) {
118
- for (var w = 0; w < SYNONYM_GROUPS[g].length; w++) {
119
- var word = SYNONYM_GROUPS[g][w].toLowerCase();
120
- if (!synonymLookup[word]) synonymLookup[word] = [];
121
- synonymLookup[word].push(g);
122
- }
123
- }
124
-
125
- // --- Preprocessing functions ---
126
-
127
- function tokenize(text, lang) {
128
- var s = text.toLowerCase();
129
- if (lang === 'vi') s = removeDiacritics(s);
130
- s = s.replace(/[?!.,;:"""''`~()[\]{}\\|@#$%^&]/g, ' ');
131
- return s.split(/\s+/).filter(function(w) { return w.length > 0; });
132
- }
133
-
134
- function buildTF(tokens) {
135
- var tf = {};
136
- for (var i = 0; i < tokens.length; i++) {
137
- tf[tokens[i]] = (tf[tokens[i]] || 0) + 1;
138
- }
139
- return tf;
140
- }
141
-
142
- function getSynonymGroupIndices(tokens) {
143
- var indices = {};
144
- for (var i = 0; i < tokens.length; i++) {
145
- var groups = synonymLookup[tokens[i]];
146
- if (groups) {
147
- for (var j = 0; j < groups.length; j++) {
148
- indices[groups[j]] = true;
149
- }
150
- }
151
- }
152
- return Object.keys(indices).map(Number);
153
- }
154
-
155
- // --- Build IDF from entire corpus ---
156
-
157
- function buildCorpusIDF(allDocs) {
158
- var docCount = allDocs.length;
159
- var df = {}; // document frequency: word → number of docs containing it
160
- for (var d = 0; d < allDocs.length; d++) {
161
- var seen = {};
162
- for (var t = 0; t < allDocs[d].length; t++) {
163
- var word = allDocs[d][t];
164
- if (!seen[word]) {
165
- df[word] = (df[word] || 0) + 1;
166
- seen[word] = true;
167
- }
168
- }
169
- }
170
- // IDF = log(N / df) + 1 (smoothed)
171
- var idf = {};
172
- for (var w in df) {
173
- idf[w] = Math.log(docCount / df[w]) + 1;
174
- }
175
- return idf;
176
- }
177
-
178
- function buildTFIDF(tf, idf) {
179
- var tfidf = {};
180
- for (var word in tf) {
181
- tfidf[word] = tf[word] * (idf[word] || 1);
182
- }
183
- return tfidf;
184
- }
185
-
186
- function vectorMagnitude(vec) {
187
- var sum = 0;
188
- for (var k in vec) sum += vec[k] * vec[k];
189
- return Math.sqrt(sum);
190
- }
191
-
192
- // --- Main preprocessing ---
193
-
194
- console.log('Preprocessing data...');
195
-
196
- var output = { version: Date.now(), langs: {} };
197
-
198
- var LANGS = ['vi', 'en', 'ja'];
199
-
200
- for (var li = 0; li < LANGS.length; li++) {
201
- var lang = LANGS[li];
202
- console.log(' Processing language:', lang);
203
-
204
- // Collect all statements (QA questions + specific response keys + brain triggers)
205
- var statements = [];
206
-
207
- // From QA dataset
208
- var qaData = QA_DATASET[lang] || [];
209
- for (var qi = 0; qi < qaData.length; qi++) {
210
- statements.push({ source: 'qa', index: qi, text: qaData[qi].q, answer: qaData[qi].a });
211
- }
212
-
213
- // From specific responses
214
- var specData = SPECIFIC_RESPONSES[lang] || {};
215
- var specKeys = Object.keys(specData);
216
- for (var si = 0; si < specKeys.length; si++) {
217
- statements.push({ source: 'specific', index: si, text: specKeys[si], answer: specData[specKeys[si]] });
218
- }
219
-
220
- // From brain .rive files (trigger-response pairs)
221
- var brainFile = path.join(BRAIN_DIR, lang + '.rive');
222
- if (fs.existsSync(brainFile)) {
223
- var brainPairs = parseBrainFile(brainFile);
224
- var seenTriggers = {};
225
- for (var bi = 0; bi < brainPairs.length; bi++) {
226
- var trigger = brainPairs[bi].trigger;
227
- // Bỏ wildcard suffix/prefix để lấy phần text chính (ví dụ: "xin chao *" → "xin chao")
228
- var cleanTrigger = trigger.replace(/\*/g, '').trim();
229
- if (cleanTrigger.length < 2) continue;
230
- // Dedup: chỉ giữ trigger đầu tiên nếu trùng text
231
- if (seenTriggers[cleanTrigger]) continue;
232
- seenTriggers[cleanTrigger] = true;
233
- statements.push({ source: 'brain', index: bi, text: cleanTrigger, answer: brainPairs[bi].response });
234
- }
235
- console.log(' Brain triggers:', Object.keys(seenTriggers).length);
236
- }
237
-
238
- // Tokenize all statements
239
- var allTokens = [];
240
- var processed = [];
241
- for (var i = 0; i < statements.length; i++) {
242
- var stmt = statements[i];
243
- var tokens = tokenize(stmt.text, lang);
244
- var tf = buildTF(tokens);
245
- var synGroups = getSynonymGroupIndices(tokens);
246
-
247
- allTokens.push(tokens);
248
- processed.push({
249
- source: stmt.source,
250
- originalText: stmt.text,
251
- answer: stmt.answer,
252
- tokens: tokens,
253
- tf: tf,
254
- synGroups: synGroups
255
- // tfidf and magnitude will be added after IDF is computed
256
- });
257
- }
258
-
259
- // Build IDF from all documents in this language
260
- var idf = buildCorpusIDF(allTokens);
261
-
262
- // Compute TF-IDF vectors and magnitudes
263
- for (var j = 0; j < processed.length; j++) {
264
- processed[j].tfidf = buildTFIDF(processed[j].tf, idf);
265
- processed[j].magnitude = vectorMagnitude(processed[j].tfidf);
266
- }
267
-
268
- output.langs[lang] = {
269
- idf: idf,
270
- statements: processed
271
- };
272
-
273
- console.log(' Statements:', processed.length, '| Vocab size:', Object.keys(idf).length);
274
- }
275
-
276
- // Write output
277
- var outputPath = path.join(ROOT, 'data', 'preprocessed.json');
278
- fs.writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf8');
279
- console.log('Done! Output:', outputPath);
280
- console.log('Version:', output.version);
 
1
+ #!/usr/bin/env node
2
+ // ============================================================
3
+ // scripts/preprocess.js — Tiền xử lý dữ liệu cho text similarity
4
+ //
5
+ // Chạy: node scripts/preprocess.js
6
+ // Output: data/preprocessed.json
7
+ //
8
+ // Script này đọc QA dataset + specific responses + brain .rive files, thực hiện:
9
+ // 1. Tokenize (tách từ)
10
+ // 2. Normalize (lowercase, bỏ dấu tiếng Việt, bỏ punctuation)
11
+ // 3. Tính TF vector cho mỗi câu hỏi
12
+ // 4. Tính synonym group indices cho mỗi từ
13
+ // 5. Tạo IDF (inverse document frequency) cho toàn bộ corpus
14
+ // 6. Tính TF-IDF vector cho mỗi câu hỏi
15
+ // 7. Lưu tất cả vào data/preprocessed.json
16
+ //
17
+ // Khi dữ liệu thay đổi (qa-dataset.json, specific-responses.json, brain/*.rive),
18
+ // chạy lại script này để tạo preprocessed.json mới.
19
+ // ============================================================
20
+
21
+ var fs = require('fs');
22
+ var path = require('path');
23
+
24
+ var ROOT = path.join(__dirname, '..');
25
+ var QA_DATASET = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'qa-dataset.json'), 'utf8'));
26
+ var SPECIFIC_RESPONSES = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'specific-responses.json'), 'utf8'));
27
+
28
+ // --- Parse brain .rive files to extract trigger-response pairs ---
29
+ var BRAIN_DIR = path.join(ROOT, 'brain');
30
+
31
+ /**
32
+ * Parse file .rive và trích xuất các cặp trigger-response.
33
+ * Bỏ qua: trigger wildcard mặc định (*), trigger chứa <call>, trigger chỉ có wildcard.
34
+ */
35
+ function parseBrainFile(filePath) {
36
+ var content = fs.readFileSync(filePath, 'utf8');
37
+ var lines = content.split('\n');
38
+ var pairs = [];
39
+ var currentTrigger = null;
40
+
41
+ for (var i = 0; i < lines.length; i++) {
42
+ var line = lines[i].trim();
43
+
44
+ // Skip comments and empty lines
45
+ if (line.indexOf('//') === 0 || line.length === 0) continue;
46
+ // Skip directives (! version, ! var)
47
+ if (line.indexOf('!') === 0) continue;
48
+
49
+ if (line.indexOf('+ ') === 0) {
50
+ currentTrigger = line.substring(2).trim();
51
+ } else if (line.indexOf('- ') === 0 && currentTrigger) {
52
+ var response = line.substring(2).trim();
53
+
54
+ // Bỏ qua trigger wildcard mặc định
55
+ if (currentTrigger === '*') { currentTrigger = null; continue; }
56
+ // Bỏ qua response chứa <call> (adapter calls, không phải text response)
57
+ if (response.indexOf('<call>') !== -1) { currentTrigger = null; continue; }
58
+ // Bỏ qua trigger chỉ chứa wildcard (ví dụ: "* la ai")
59
+ var nonWild = currentTrigger.replace(/\*/g, '').trim();
60
+ if (nonWild.length < 2) { currentTrigger = null; continue; }
61
+
62
+ pairs.push({ trigger: currentTrigger, response: response });
63
+ currentTrigger = null;
64
+ }
65
+ }
66
+ return pairs;
67
+ }
68
+
69
+ // --- Vietnamese diacritics removal (copy from app.js) ---
70
+ var VIETNAMESE_DIACRITICS_MAP = {
71
+ 'à':'a','á':'a','ả':'a','ã':'a','ạ':'a',
72
+ 'ă':'a','ằ':'a','ắ':'a','ẳ':'a','ẵ':'a','ặ':'a',
73
+ 'â':'a','ầ':'a','ấ':'a','ẩ':'a','ẫ':'a','ậ':'a',
74
+ 'đ':'d',
75
+ 'è':'e','é':'e','ẻ':'e','ẽ':'e','ẹ':'e',
76
+ 'ê':'e','ề':'e','ế':'e','ể':'e','ễ':'e','ệ':'e',
77
+ 'ì':'i','í':'i','ỉ':'i','ĩ':'i','ị':'i',
78
+ 'ò':'o','ó':'o','ỏ':'o','õ':'o','ọ':'o',
79
+ 'ô':'o','ồ':'o','ố':'o','ổ':'o','ỗ':'o','ộ':'o',
80
+ 'ơ':'o','ờ':'o','ớ':'o','ở':'o','ỡ':'o','ợ':'o',
81
+ 'ù':'u','ú':'u','ủ':'u','ũ':'u','ụ':'u',
82
+ 'ư':'u','ừ':'u','ứ':'u','ử':'u','ữ':'u','ự':'u',
83
+ 'ỳ':'y','ý':'y','ỷ':'y','ỹ':'y','ỵ':'y'
84
+ };
85
+
86
+ function removeDiacritics(str) {
87
+ var result = '';
88
+ for (var i = 0; i < str.length; i++) {
89
+ var ch = str[i];
90
+ result += VIETNAMESE_DIACRITICS_MAP[ch] || ch;
91
+ }
92
+ return result;
93
+ }
94
+
95
+ // --- Synonym groups (copy from text-similarity.js) ---
96
+ var SYNONYM_GROUPS = [
97
+ ['xin chao','chao','hi','hello','hey','yo'],
98
+ ['tam biet','bye','goodbye','hen gap lai','tot lanh'],
99
+ ['cam on','thanks','thank','thank you'],
100
+ ['ten','name','ai','who','la ai'],
101
+ ['lam gi','lam duoc','co the','giup','help','what can'],
102
+ ['may gio','gio','time','clock','bao gio'],
103
+ ['ngay','hom nay','date','today','ngay may'],
104
+ ['thu','thu may','day','what day'],
105
+ ['tinh','calculate','math','cong','tru','nhan','chia','plus','minus'],
106
+ ['doi','convert','sang','to','chuyen doi'],
107
+ ['chatbot','bot','ai','robot','may','machine'],
108
+ ['la gi','what is','what','gi','mean'],
109
+ ['khoe','vui','happy','fine','good','ok'],
110
+ ['tuoi','age','old','bao nhieu tuoi'],
111
+ ['o dau','where','dau','location'],
112
+ ['thich','like','love','yeu'],
113
+ ['huong dan','cach','how','guide','help','su dung','use']
114
+ ];
115
+
116
+ var synonymLookup = {};
117
+ for (var g = 0; g < SYNONYM_GROUPS.length; g++) {
118
+ for (var w = 0; w < SYNONYM_GROUPS[g].length; w++) {
119
+ var word = SYNONYM_GROUPS[g][w].toLowerCase();
120
+ if (!synonymLookup[word]) synonymLookup[word] = [];
121
+ synonymLookup[word].push(g);
122
+ }
123
+ }
124
+
125
+ // --- Preprocessing functions ---
126
+
127
+ function tokenize(text, lang) {
128
+ var s = text.toLowerCase();
129
+ if (lang === 'vi') s = removeDiacritics(s);
130
+ s = s.replace(/[?!.,;:"""''`~()[\]{}\\|@#$%^&]/g, ' ');
131
+ return s.split(/\s+/).filter(function(w) { return w.length > 0; });
132
+ }
133
+
134
+ function buildTF(tokens) {
135
+ var tf = {};
136
+ for (var i = 0; i < tokens.length; i++) {
137
+ tf[tokens[i]] = (tf[tokens[i]] || 0) + 1;
138
+ }
139
+ return tf;
140
+ }
141
+
142
+ function getSynonymGroupIndices(tokens) {
143
+ var indices = {};
144
+ for (var i = 0; i < tokens.length; i++) {
145
+ var groups = synonymLookup[tokens[i]];
146
+ if (groups) {
147
+ for (var j = 0; j < groups.length; j++) {
148
+ indices[groups[j]] = true;
149
+ }
150
+ }
151
+ }
152
+ return Object.keys(indices).map(Number);
153
+ }
154
+
155
+ // --- Build IDF from entire corpus ---
156
+
157
+ function buildCorpusIDF(allDocs) {
158
+ var docCount = allDocs.length;
159
+ var df = {}; // document frequency: word → number of docs containing it
160
+ for (var d = 0; d < allDocs.length; d++) {
161
+ var seen = {};
162
+ for (var t = 0; t < allDocs[d].length; t++) {
163
+ var word = allDocs[d][t];
164
+ if (!seen[word]) {
165
+ df[word] = (df[word] || 0) + 1;
166
+ seen[word] = true;
167
+ }
168
+ }
169
+ }
170
+ // IDF = log(N / df) + 1 (smoothed)
171
+ var idf = {};
172
+ for (var w in df) {
173
+ idf[w] = Math.log(docCount / df[w]) + 1;
174
+ }
175
+ return idf;
176
+ }
177
+
178
+ function buildTFIDF(tf, idf) {
179
+ var tfidf = {};
180
+ for (var word in tf) {
181
+ tfidf[word] = tf[word] * (idf[word] || 1);
182
+ }
183
+ return tfidf;
184
+ }
185
+
186
+ function vectorMagnitude(vec) {
187
+ var sum = 0;
188
+ for (var k in vec) sum += vec[k] * vec[k];
189
+ return Math.sqrt(sum);
190
+ }
191
+
192
+ // --- Main preprocessing ---
193
+
194
+ console.log('Preprocessing data...');
195
+
196
+ var output = { version: Date.now(), langs: {} };
197
+
198
+ var LANGS = ['vi', 'en', 'ja'];
199
+
200
+ for (var li = 0; li < LANGS.length; li++) {
201
+ var lang = LANGS[li];
202
+ console.log(' Processing language:', lang);
203
+
204
+ // Collect all statements (QA questions + specific response keys + brain triggers)
205
+ var statements = [];
206
+
207
+ // From QA dataset
208
+ var qaData = QA_DATASET[lang] || [];
209
+ for (var qi = 0; qi < qaData.length; qi++) {
210
+ statements.push({ source: 'qa', index: qi, text: qaData[qi].q, answer: qaData[qi].a });
211
+ }
212
+
213
+ // From specific responses
214
+ var specData = SPECIFIC_RESPONSES[lang] || {};
215
+ var specKeys = Object.keys(specData);
216
+ for (var si = 0; si < specKeys.length; si++) {
217
+ statements.push({ source: 'specific', index: si, text: specKeys[si], answer: specData[specKeys[si]] });
218
+ }
219
+
220
+ // From brain .rive files (trigger-response pairs)
221
+ var brainFile = path.join(BRAIN_DIR, lang + '.rive');
222
+ if (fs.existsSync(brainFile)) {
223
+ var brainPairs = parseBrainFile(brainFile);
224
+ var seenTriggers = {};
225
+ for (var bi = 0; bi < brainPairs.length; bi++) {
226
+ var trigger = brainPairs[bi].trigger;
227
+ // Bỏ wildcard suffix/prefix để lấy phần text chính (ví dụ: "xin chao *" → "xin chao")
228
+ var cleanTrigger = trigger.replace(/\*/g, '').trim();
229
+ if (cleanTrigger.length < 2) continue;
230
+ // Dedup: chỉ giữ trigger đầu tiên nếu trùng text
231
+ if (seenTriggers[cleanTrigger]) continue;
232
+ seenTriggers[cleanTrigger] = true;
233
+ statements.push({ source: 'brain', index: bi, text: cleanTrigger, answer: brainPairs[bi].response });
234
+ }
235
+ console.log(' Brain triggers:', Object.keys(seenTriggers).length);
236
+ }
237
+
238
+ // Tokenize all statements
239
+ var allTokens = [];
240
+ var processed = [];
241
+ for (var i = 0; i < statements.length; i++) {
242
+ var stmt = statements[i];
243
+ var tokens = tokenize(stmt.text, lang);
244
+ var tf = buildTF(tokens);
245
+ var synGroups = getSynonymGroupIndices(tokens);
246
+
247
+ allTokens.push(tokens);
248
+ processed.push({
249
+ source: stmt.source,
250
+ originalText: stmt.text,
251
+ answer: stmt.answer,
252
+ tokens: tokens,
253
+ tf: tf,
254
+ synGroups: synGroups
255
+ // tfidf and magnitude will be added after IDF is computed
256
+ });
257
+ }
258
+
259
+ // Build IDF from all documents in this language
260
+ var idf = buildCorpusIDF(allTokens);
261
+
262
+ // Compute TF-IDF vectors and magnitudes
263
+ for (var j = 0; j < processed.length; j++) {
264
+ processed[j].tfidf = buildTFIDF(processed[j].tf, idf);
265
+ processed[j].magnitude = vectorMagnitude(processed[j].tfidf);
266
+ }
267
+
268
+ output.langs[lang] = {
269
+ idf: idf,
270
+ statements: processed
271
+ };
272
+
273
+ console.log(' Statements:', processed.length, '| Vocab size:', Object.keys(idf).length);
274
+ }
275
+
276
+ // Write output
277
+ var outputPath = path.join(ROOT, 'data', 'preprocessed.json');
278
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf8');
279
+ console.log('Done! Output:', outputPath);
280
+ console.log('Version:', output.version);
style.css CHANGED
@@ -1,948 +1,1083 @@
1
- /* === Reset & Base === */
2
- *, *::before, *::after {
3
- box-sizing: border-box;
4
- margin: 0;
5
- padding: 0;
6
- }
7
-
8
- body {
9
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
10
- background-color: #f0f2f5;
11
- min-height: 100vh;
12
- display: flex;
13
- justify-content: center;
14
- align-items: center;
15
- padding: 16px;
16
- }
17
-
18
- /* === Chat Container === */
19
- .chat-container {
20
- width: 100%;
21
- max-width: 600px;
22
- height: 90vh;
23
- background-color: #ffffff;
24
- border-radius: 12px;
25
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
26
- display: flex;
27
- flex-direction: column;
28
- overflow: hidden;
29
- }
30
-
31
- /* === Chat Header === */
32
- .chat-header {
33
- display: flex;
34
- align-items: center;
35
- justify-content: space-between;
36
- padding: 12px 16px;
37
- background-color: #4a90d9;
38
- color: #ffffff;
39
- }
40
-
41
- .chat-header h1 {
42
- font-size: 1.25rem;
43
- font-weight: 600;
44
- }
45
-
46
- .header-controls {
47
- display: flex;
48
- align-items: center;
49
- gap: 8px;
50
- }
51
-
52
- .header-controls button {
53
- background: none;
54
- border: none;
55
- font-size: 1.2rem;
56
- cursor: pointer;
57
- padding: 4px;
58
- border-radius: 4px;
59
- transition: background-color 0.2s;
60
- }
61
-
62
- .header-controls button:hover {
63
- background-color: rgba(255, 255, 255, 0.2);
64
- }
65
-
66
- #language-selector {
67
- padding: 4px 8px;
68
- border: 1px solid rgba(255, 255, 255, 0.4);
69
- border-radius: 6px;
70
- background-color: rgba(255, 255, 255, 0.15);
71
- color: #ffffff;
72
- font-size: 0.85rem;
73
- cursor: pointer;
74
- }
75
-
76
- /* === Thinking Toggle === */
77
- .thinking-toggle {
78
- display: flex;
79
- align-items: center;
80
- cursor: pointer;
81
- gap: 2px;
82
- }
83
-
84
- .thinking-toggle input[type="checkbox"] {
85
- display: none;
86
- }
87
-
88
- .thinking-toggle-label {
89
- font-size: 1.2rem;
90
- opacity: 0.5;
91
- transition: opacity 0.2s;
92
- padding: 2px 4px;
93
- border-radius: 4px;
94
- }
95
-
96
- .thinking-toggle input[type="checkbox"]:checked + .thinking-toggle-label {
97
- opacity: 1;
98
- }
99
-
100
- .thinking-toggle:hover .thinking-toggle-label {
101
- background-color: rgba(255, 255, 255, 0.2);
102
- }
103
-
104
- /* === Settings Panel === */
105
- .settings-body {
106
- display: flex;
107
- flex-direction: column;
108
- gap: 10px;
109
- }
110
-
111
- .setting-item {
112
- display: flex;
113
- flex-direction: column;
114
- gap: 3px;
115
- }
116
-
117
- .setting-item label {
118
- display: flex;
119
- align-items: center;
120
- gap: 6px;
121
- font-size: 0.85rem;
122
- color: #333;
123
- cursor: pointer;
124
- }
125
-
126
- .setting-number {
127
- width: 60px;
128
- padding: 3px 6px;
129
- border: 1px solid #ccc;
130
- border-radius: 4px;
131
- font-size: 0.85rem;
132
- }
133
-
134
- .setting-hint {
135
- font-size: 0.75rem;
136
- color: #999;
137
- margin-left: 2px;
138
- }
139
-
140
- .setting-buttons {
141
- flex-direction: row;
142
- gap: 8px;
143
- }
144
-
145
- .setting-btn {
146
- padding: 5px 12px;
147
- font-size: 0.82rem;
148
- border: 1px solid #ccc;
149
- border-radius: 6px;
150
- background: #fff;
151
- cursor: pointer;
152
- transition: background-color 0.2s;
153
- }
154
-
155
- .setting-btn:hover {
156
- background-color: #f0f0f0;
157
- }
158
-
159
- .setting-btn-danger {
160
- color: #dc3545;
161
- border-color: #dc3545;
162
- }
163
-
164
- .setting-btn-danger:hover {
165
- background-color: #dc3545;
166
- color: #fff;
167
- }
168
-
169
- /* === History Dialog === */
170
- .history-dialog {
171
- max-width: 600px;
172
- max-height: 85vh;
173
- }
174
-
175
- .history-tab-buttons {
176
- display: flex;
177
- gap: 4px;
178
- margin-left: auto;
179
- margin-right: 12px;
180
- }
181
-
182
- .history-tab {
183
- padding: 3px 10px;
184
- font-size: 0.78rem;
185
- border: 1px solid rgba(255,255,255,0.4);
186
- border-radius: 12px;
187
- background: transparent;
188
- color: rgba(255,255,255,0.7);
189
- cursor: pointer;
190
- transition: background-color 0.2s, color 0.2s;
191
- }
192
-
193
- .history-tab.active {
194
- background: rgba(255,255,255,0.25);
195
- color: #fff;
196
- }
197
-
198
- .history-list {
199
- display: flex;
200
- flex-direction: column;
201
- gap: 8px;
202
- }
203
-
204
- .history-item {
205
- padding: 8px 10px;
206
- border-radius: 8px;
207
- border: 1px solid #e0e0e0;
208
- }
209
-
210
- .history-item-user {
211
- background-color: #e3f2fd;
212
- }
213
-
214
- .history-item-assistant {
215
- background-color: #f5f5f5;
216
- }
217
-
218
- .history-meta {
219
- display: block;
220
- font-size: 0.72rem;
221
- color: #888;
222
- margin-bottom: 3px;
223
- }
224
-
225
- .history-content {
226
- display: block;
227
- font-size: 0.85rem;
228
- color: #333;
229
- white-space: pre-wrap;
230
- word-wrap: break-word;
231
- max-height: 80px;
232
- overflow: hidden;
233
- text-overflow: ellipsis;
234
- }
235
-
236
- .history-empty {
237
- text-align: center;
238
- color: #999;
239
- font-size: 0.88rem;
240
- padding: 20px 0;
241
- }
242
-
243
- .history-paging {
244
- display: flex;
245
- align-items: center;
246
- justify-content: center;
247
- gap: 10px;
248
- padding: 10px 0 0;
249
- }
250
-
251
- .history-page-info {
252
- font-size: 0.78rem;
253
- color: #888;
254
- }
255
-
256
- .history-page-btn {
257
- padding: 4px 12px;
258
- font-size: 0.8rem;
259
- border: 1px solid #ccc;
260
- border-radius: 4px;
261
- background: #fff;
262
- cursor: pointer;
263
- }
264
-
265
- .history-page-btn:hover {
266
- background-color: #f0f0f0;
267
- }
268
-
269
- #language-selector option {
270
- color: #333333;
271
- background-color: #ffffff;
272
- }
273
-
274
- /* === Slide Panel (Rules & Macros) === */
275
- .slide-panel {
276
- border-bottom: 1px solid #e0e0e0;
277
- background-color: #f8f9fa;
278
- max-height: 280px;
279
- display: flex;
280
- flex-direction: column;
281
- animation: slideDown 0.2s ease-out;
282
- }
283
-
284
- @keyframes slideDown {
285
- from { max-height: 0; opacity: 0; }
286
- to { max-height: 280px; opacity: 1; }
287
- }
288
-
289
- .slide-panel-header {
290
- display: flex;
291
- align-items: center;
292
- justify-content: space-between;
293
- padding: 10px 16px;
294
- background-color: #eef2f7;
295
- border-bottom: 1px solid #dde3ea;
296
- flex-shrink: 0;
297
- }
298
-
299
- .slide-panel-header h3 {
300
- font-size: 0.88rem;
301
- font-weight: 600;
302
- color: #4a90d9;
303
- margin: 0;
304
- }
305
-
306
- .slide-panel-close {
307
- background: none;
308
- border: none;
309
- font-size: 1.2rem;
310
- color: #888888;
311
- cursor: pointer;
312
- padding: 0 4px;
313
- line-height: 1;
314
- border-radius: 4px;
315
- transition: color 0.2s, background-color 0.2s;
316
- }
317
-
318
- .slide-panel-close:hover {
319
- color: #333333;
320
- background-color: rgba(0, 0, 0, 0.08);
321
- }
322
-
323
- .slide-panel-body {
324
- overflow-y: auto;
325
- padding: 8px 16px 12px;
326
- flex: 1;
327
- }
328
-
329
- /* === Rules List === */
330
- .rules-list {
331
- list-style: none;
332
- padding: 0;
333
- margin: 0;
334
- display: flex;
335
- flex-wrap: wrap;
336
- gap: 6px;
337
- }
338
-
339
- .rules-list li {
340
- display: inline-block;
341
- padding: 4px 10px;
342
- font-size: 0.82rem;
343
- color: #4a6785;
344
- background-color: #e8eef5;
345
- border-radius: 12px;
346
- border: 1px solid #d0dae6;
347
- cursor: default;
348
- transition: background-color 0.15s;
349
- }
350
-
351
- .rules-list li:hover {
352
- background-color: #dce4ee;
353
- }
354
-
355
- /* === Macros List === */
356
- .macros-list {
357
- list-style: none;
358
- padding: 0;
359
- margin: 0;
360
- display: flex;
361
- flex-direction: column;
362
- gap: 8px;
363
- }
364
-
365
- .macro-item {
366
- padding: 10px 12px;
367
- background-color: #ffffff;
368
- border-radius: 8px;
369
- border: 1px solid #e0e5ec;
370
- transition: box-shadow 0.15s;
371
- }
372
-
373
- .macro-item:hover {
374
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
375
- }
376
-
377
- .macro-item-header {
378
- display: flex;
379
- align-items: center;
380
- justify-content: space-between;
381
- margin-bottom: 4px;
382
- }
383
-
384
- .macro-item strong {
385
- font-size: 0.85rem;
386
- color: #333333;
387
- }
388
-
389
- .macro-item-badge {
390
- display: inline-block;
391
- font-size: 0.68rem;
392
- padding: 1px 6px;
393
- border-radius: 8px;
394
- background-color: #d4edda;
395
- color: #28a745;
396
- font-weight: 500;
397
- }
398
-
399
- .macro-item p {
400
- font-size: 0.8rem;
401
- color: #666666;
402
- margin: 2px 0 6px;
403
- line-height: 1.4;
404
- }
405
-
406
- .macro-call-syntax {
407
- display: inline-block;
408
- font-family: 'Courier New', Courier, monospace;
409
- font-size: 0.76rem;
410
- background-color: #f0f0f0;
411
- color: #c7254e;
412
- padding: 3px 8px;
413
- border-radius: 4px;
414
- border: 1px solid #e0e0e0;
415
- }
416
-
417
- /* === Message Display === */
418
- .message-display {
419
- flex: 1;
420
- overflow-y: auto;
421
- padding: 16px;
422
- display: flex;
423
- flex-direction: column;
424
- gap: 10px;
425
- }
426
-
427
- /* === Messages === */
428
- .message {
429
- max-width: 80%;
430
- padding: 10px 14px;
431
- border-radius: 12px;
432
- word-wrap: break-word;
433
- line-height: 1.45;
434
- font-size: 0.95rem;
435
- }
436
-
437
- .message.user {
438
- align-self: flex-end;
439
- background-color: #4a90d9;
440
- color: #ffffff;
441
- border-bottom-right-radius: 4px;
442
- }
443
-
444
- .message.bot {
445
- align-self: flex-start;
446
- background-color: #e9ecef;
447
- color: #333333;
448
- border-bottom-left-radius: 4px;
449
- }
450
-
451
- /* === Confidence === */
452
- .confidence {
453
- display: block;
454
- font-size: 0.72rem;
455
- margin-top: 4px;
456
- font-weight: 500;
457
- }
458
-
459
- .confidence-high {
460
- color: #28a745;
461
- }
462
-
463
- .confidence-low {
464
- color: #dc3545;
465
- }
466
-
467
- /* === Adapter Path Breadcrumb === */
468
- .adapter-path {
469
- display: block;
470
- font-size: 0.7rem;
471
- margin-top: 2px;
472
- color: #8e8e8e;
473
- font-style: italic;
474
- }
475
-
476
- /* === Response Time === */
477
- .response-time {
478
- display: block;
479
- font-size: 0.68rem;
480
- margin-top: 2px;
481
- color: #aaaaaa;
482
- }
483
-
484
- /* === Links in bot messages === */
485
- .message.bot a {
486
- color: #4a90d9;
487
- text-decoration: underline;
488
- word-break: break-all;
489
- }
490
-
491
- .message.bot a:hover {
492
- color: #3a7bc8;
493
- }
494
-
495
- /* === Loading Indicator === */
496
- .loading-indicator {
497
- align-self: flex-start;
498
- padding: 10px 14px;
499
- background-color: #e9ecef;
500
- border-radius: 12px;
501
- font-size: 0.9rem;
502
- color: #888888;
503
- animation: pulse 1.2s ease-in-out infinite;
504
- }
505
-
506
- @keyframes pulse {
507
- 0%, 100% { opacity: 1; }
508
- 50% { opacity: 0.5; }
509
- }
510
-
511
- /* === Input Area === */
512
- .input-area {
513
- display: flex;
514
- padding: 12px 16px;
515
- border-top: 1px solid #e0e0e0;
516
- background-color: #ffffff;
517
- gap: 8px;
518
- }
519
-
520
- .input-area input {
521
- flex: 1;
522
- padding: 10px 14px;
523
- border: 1px solid #cccccc;
524
- border-radius: 20px;
525
- font-size: 0.95rem;
526
- outline: none;
527
- transition: border-color 0.2s;
528
- }
529
-
530
- .input-area input:focus {
531
- border-color: #4a90d9;
532
- }
533
-
534
- .input-area button {
535
- padding: 10px 20px;
536
- background-color: #4a90d9;
537
- color: #ffffff;
538
- border: none;
539
- border-radius: 20px;
540
- font-size: 0.95rem;
541
- cursor: pointer;
542
- transition: background-color 0.2s;
543
- white-space: nowrap;
544
- }
545
-
546
- .input-area button:hover {
547
- background-color: #3a7bc8;
548
- }
549
-
550
- .input-area button:disabled,
551
- .input-area button.disabled {
552
- background-color: #a0bfdf;
553
- cursor: not-allowed;
554
- opacity: 0.7;
555
- }
556
-
557
- .input-area button:disabled:hover,
558
- .input-area button.disabled:hover {
559
- background-color: #a0bfdf;
560
- }
561
-
562
- /* === LLM Loading Status === */
563
- .llm-loading-status {
564
- align-self: flex-start;
565
- padding: 10px 14px;
566
- background-color: #fff3cd;
567
- border: 1px solid #ffc107;
568
- border-radius: 12px;
569
- font-size: 0.88rem;
570
- color: #856404;
571
- animation: pulse 1.5s ease-in-out infinite;
572
- }
573
-
574
- /* === Attachment Preview === */
575
- .attachment-preview {
576
- display: flex;
577
- align-items: center;
578
- gap: 8px;
579
- padding: 6px 16px;
580
- background-color: #f8f9fa;
581
- border-top: 1px solid #e0e0e0;
582
- }
583
-
584
- .attachment-thumb {
585
- width: 40px;
586
- height: 40px;
587
- object-fit: cover;
588
- border-radius: 6px;
589
- border: 1px solid #ddd;
590
- }
591
-
592
- .attachment-name {
593
- flex: 1;
594
- font-size: 0.82rem;
595
- color: #555;
596
- overflow: hidden;
597
- text-overflow: ellipsis;
598
- white-space: nowrap;
599
- }
600
-
601
- .attachment-remove {
602
- background: none;
603
- border: none;
604
- font-size: 1.2rem;
605
- color: #999;
606
- cursor: pointer;
607
- padding: 2px 6px;
608
- border-radius: 4px;
609
- line-height: 1;
610
- transition: color 0.2s, background-color 0.2s;
611
- }
612
-
613
- .attachment-remove:hover {
614
- color: #dc3545;
615
- background-color: rgba(220, 53, 69, 0.1);
616
- }
617
-
618
- /* === Attach Button === */
619
- #attach-button {
620
- background: none;
621
- border: none;
622
- font-size: 1.3rem;
623
- cursor: pointer;
624
- padding: 6px;
625
- border-radius: 50%;
626
- transition: background-color 0.2s;
627
- flex-shrink: 0;
628
- }
629
-
630
- #attach-button:hover {
631
- background-color: #e9ecef;
632
- }
633
-
634
- /* === Message Image === */
635
- .message-image {
636
- display: block;
637
- max-width: 100%;
638
- max-height: 200px;
639
- border-radius: 8px;
640
- margin-bottom: 6px;
641
- object-fit: contain;
642
- }
643
-
644
- /* === LLM Cancel Button === */
645
- .llm-cancel-container {
646
- align-self: flex-start;
647
- margin-top: 4px;
648
- }
649
-
650
- .llm-cancel-button {
651
- display: inline-block;
652
- padding: 5px 14px;
653
- font-size: 0.82rem;
654
- color: #dc3545;
655
- background-color: #fff;
656
- border: 1px solid #dc3545;
657
- border-radius: 16px;
658
- cursor: pointer;
659
- transition: background-color 0.2s, color 0.2s;
660
- }
661
-
662
- .llm-cancel-button:hover {
663
- background-color: #dc3545;
664
- color: #fff;
665
- }
666
-
667
- /* === Streaming Message === */
668
- .message-text {
669
- white-space: pre-wrap;
670
- word-wrap: break-word;
671
- }
672
-
673
- .message.bot.streaming .message-text {
674
- border-right: 2px solid #4a90d9;
675
- padding-right: 2px;
676
- animation: blink-cursor 0.8s step-end infinite;
677
- }
678
-
679
- @keyframes blink-cursor {
680
- 50% { border-color: transparent; }
681
- }
682
-
683
- /* === LLM Thinking Block === */
684
- .llm-thinking-block {
685
- background-color: #e8f5e9;
686
- border-left: 3px solid #81c784;
687
- border-radius: 6px;
688
- padding: 8px 10px;
689
- margin-bottom: 8px;
690
- font-size: 0.84rem;
691
- line-height: 1.5;
692
- color: #2e7d32;
693
- }
694
-
695
- .llm-thinking-label {
696
- display: block;
697
- font-weight: 600;
698
- font-size: 0.78rem;
699
- margin-bottom: 4px;
700
- color: #388e3c;
701
- }
702
-
703
- .llm-thinking-content {
704
- display: block;
705
- white-space: pre-wrap;
706
- word-wrap: break-word;
707
- color: #1b5e20;
708
- }
709
-
710
- /* === Utility === */
711
- .hidden {
712
- display: none !important;
713
- }
714
-
715
- /* === Responsive — Mobile (max-width: 768px, min-width: 320px) === */
716
- @media (max-width: 768px) {
717
- body {
718
- padding: 0;
719
- align-items: stretch;
720
- }
721
-
722
- .chat-container {
723
- max-width: 100%;
724
- height: 100vh;
725
- border-radius: 0;
726
- box-shadow: none;
727
- }
728
-
729
- .chat-header {
730
- padding: 10px 12px;
731
- }
732
-
733
- .chat-header h1 {
734
- font-size: 1.1rem;
735
- }
736
-
737
- .header-controls {
738
- gap: 6px;
739
- }
740
-
741
- #language-selector {
742
- font-size: 0.8rem;
743
- padding: 3px 6px;
744
- }
745
-
746
- .message-display {
747
- padding: 12px;
748
- }
749
-
750
- .message {
751
- max-width: 88%;
752
- font-size: 0.9rem;
753
- }
754
-
755
- .input-area {
756
- padding: 10px 12px;
757
- }
758
-
759
- .input-area input {
760
- padding: 8px 12px;
761
- font-size: 0.9rem;
762
- }
763
-
764
- .input-area button {
765
- padding: 8px 16px;
766
- font-size: 0.9rem;
767
- }
768
-
769
- .rules-panel,
770
- .macros-panel,
771
- .slide-panel {
772
- max-height: 200px;
773
- }
774
- }
775
-
776
- /* === Help Dialog Overlay === */
777
- .help-overlay {
778
- position: fixed;
779
- top: 0;
780
- left: 0;
781
- width: 100%;
782
- height: 100%;
783
- background-color: rgba(0, 0, 0, 0.5);
784
- display: flex;
785
- justify-content: center;
786
- align-items: center;
787
- z-index: 1000;
788
- animation: fadeIn 0.2s ease-out;
789
- }
790
-
791
- @keyframes fadeIn {
792
- from { opacity: 0; }
793
- to { opacity: 1; }
794
- }
795
-
796
- .help-dialog {
797
- background-color: #ffffff;
798
- border-radius: 12px;
799
- width: 90%;
800
- max-width: 520px;
801
- max-height: 80vh;
802
- display: flex;
803
- flex-direction: column;
804
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
805
- animation: slideUp 0.25s ease-out;
806
- }
807
-
808
- @keyframes slideUp {
809
- from { transform: translateY(24px); opacity: 0; }
810
- to { transform: translateY(0); opacity: 1; }
811
- }
812
-
813
- .help-dialog-header {
814
- display: flex;
815
- align-items: center;
816
- justify-content: space-between;
817
- padding: 16px 20px;
818
- border-bottom: 1px solid #e0e0e0;
819
- background: linear-gradient(135deg, #4a90d9, #5ba3e6);
820
- color: #ffffff;
821
- border-radius: 12px 12px 0 0;
822
- }
823
-
824
- .help-dialog-header h2 {
825
- font-size: 1.05rem;
826
- font-weight: 600;
827
- margin: 0;
828
- }
829
-
830
- .help-close-button {
831
- background: none;
832
- border: none;
833
- color: #ffffff;
834
- font-size: 1.5rem;
835
- cursor: pointer;
836
- padding: 0 4px;
837
- line-height: 1;
838
- border-radius: 4px;
839
- transition: background-color 0.2s;
840
- }
841
-
842
- .help-close-button:hover {
843
- background-color: rgba(255, 255, 255, 0.2);
844
- }
845
-
846
- .help-dialog-body {
847
- padding: 20px;
848
- overflow-y: auto;
849
- font-size: 0.9rem;
850
- line-height: 1.6;
851
- color: #333333;
852
- }
853
-
854
- .help-section {
855
- margin-bottom: 18px;
856
- padding-bottom: 16px;
857
- border-bottom: 1px solid #f0f0f0;
858
- }
859
-
860
- .help-section:last-child {
861
- margin-bottom: 0;
862
- padding-bottom: 0;
863
- border-bottom: none;
864
- }
865
-
866
- .help-section h3 {
867
- font-size: 0.92rem;
868
- font-weight: 600;
869
- color: #4a90d9;
870
- margin-bottom: 8px;
871
- display: flex;
872
- align-items: center;
873
- gap: 6px;
874
- }
875
-
876
- .help-section p {
877
- margin: 4px 0;
878
- color: #555555;
879
- }
880
-
881
- .help-section ul {
882
- list-style: none;
883
- padding: 0;
884
- margin: 6px 0 0 0;
885
- }
886
-
887
- .help-section li {
888
- padding: 4px 0 4px 16px;
889
- position: relative;
890
- color: #555555;
891
- }
892
-
893
- .help-section li::before {
894
- content: '•';
895
- position: absolute;
896
- left: 4px;
897
- color: #4a90d9;
898
- font-weight: bold;
899
- }
900
-
901
- .help-example {
902
- display: inline-block;
903
- background-color: #f0f4f8;
904
- color: #4a90d9;
905
- padding: 2px 8px;
906
- border-radius: 4px;
907
- font-size: 0.84rem;
908
- font-family: 'Courier New', Courier, monospace;
909
- }
910
-
911
- .help-confidence-demo {
912
- display: flex;
913
- gap: 12px;
914
- margin-top: 8px;
915
- }
916
-
917
- .help-confidence-item {
918
- display: flex;
919
- align-items: center;
920
- gap: 6px;
921
- font-size: 0.84rem;
922
- }
923
-
924
- .help-dot {
925
- width: 10px;
926
- height: 10px;
927
- border-radius: 50%;
928
- display: inline-block;
929
- }
930
-
931
- .help-dot.green { background-color: #28a745; }
932
- .help-dot.red { background-color: #dc3545; }
933
-
934
- /* === Responsive — Help Dialog Mobile === */
935
- @media (max-width: 768px) {
936
- .help-dialog {
937
- width: 95%;
938
- max-height: 85vh;
939
- }
940
-
941
- .help-dialog-header {
942
- padding: 12px 16px;
943
- }
944
-
945
- .help-dialog-body {
946
- padding: 16px;
947
- }
948
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* === Reset & Base === */
2
+ *, *::before, *::after {
3
+ box-sizing: border-box;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
10
+ background-color: #f0f2f5;
11
+ min-height: 100vh;
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ padding: 16px;
16
+ }
17
+
18
+ /* === Chat Container === */
19
+ .chat-container {
20
+ width: 100%;
21
+ max-width: 600px;
22
+ height: 90vh;
23
+ background-color: #ffffff;
24
+ border-radius: 12px;
25
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
26
+ display: flex;
27
+ flex-direction: column;
28
+ overflow: hidden;
29
+ }
30
+
31
+ /* === Chat Header === */
32
+ .chat-header {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ padding: 12px 16px;
37
+ background-color: #4a90d9;
38
+ color: #ffffff;
39
+ }
40
+
41
+ .chat-header h1 {
42
+ font-size: 1.25rem;
43
+ font-weight: 600;
44
+ }
45
+
46
+ .header-controls {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ }
51
+
52
+ .header-controls button {
53
+ background: none;
54
+ border: none;
55
+ font-size: 1.2rem;
56
+ cursor: pointer;
57
+ padding: 4px;
58
+ border-radius: 4px;
59
+ transition: background-color 0.2s;
60
+ }
61
+
62
+ .header-controls button:hover {
63
+ background-color: rgba(255, 255, 255, 0.2);
64
+ }
65
+
66
+ #language-selector {
67
+ padding: 4px 8px;
68
+ border: 1px solid rgba(255, 255, 255, 0.4);
69
+ border-radius: 6px;
70
+ background-color: rgba(255, 255, 255, 0.15);
71
+ color: #ffffff;
72
+ font-size: 0.85rem;
73
+ cursor: pointer;
74
+ }
75
+
76
+ /* === Thinking Toggle === */
77
+ .thinking-toggle {
78
+ display: flex;
79
+ align-items: center;
80
+ cursor: pointer;
81
+ gap: 2px;
82
+ }
83
+
84
+ .thinking-toggle input[type="checkbox"] {
85
+ display: none;
86
+ }
87
+
88
+ .thinking-toggle-label {
89
+ font-size: 1.2rem;
90
+ opacity: 0.5;
91
+ transition: opacity 0.2s;
92
+ padding: 2px 4px;
93
+ border-radius: 4px;
94
+ }
95
+
96
+ .thinking-toggle input[type="checkbox"]:checked + .thinking-toggle-label {
97
+ opacity: 1;
98
+ }
99
+
100
+ .thinking-toggle:hover .thinking-toggle-label {
101
+ background-color: rgba(255, 255, 255, 0.2);
102
+ }
103
+
104
+ /* === Settings Panel === */
105
+ .settings-body {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 10px;
109
+ }
110
+
111
+ .setting-item {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 3px;
115
+ }
116
+
117
+ .setting-item label {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 6px;
121
+ font-size: 0.85rem;
122
+ color: #333;
123
+ cursor: pointer;
124
+ }
125
+
126
+ .setting-number {
127
+ width: 60px;
128
+ padding: 3px 6px;
129
+ border: 1px solid #ccc;
130
+ border-radius: 4px;
131
+ font-size: 0.85rem;
132
+ }
133
+
134
+ .setting-hint {
135
+ font-size: 0.75rem;
136
+ color: #999;
137
+ margin-left: 2px;
138
+ }
139
+
140
+ .setting-buttons {
141
+ flex-direction: row;
142
+ gap: 8px;
143
+ }
144
+
145
+ .setting-btn {
146
+ padding: 5px 12px;
147
+ font-size: 0.82rem;
148
+ border: 1px solid #ccc;
149
+ border-radius: 6px;
150
+ background: #fff;
151
+ cursor: pointer;
152
+ transition: background-color 0.2s;
153
+ }
154
+
155
+ .setting-btn:hover {
156
+ background-color: #f0f0f0;
157
+ }
158
+
159
+ .setting-btn-danger {
160
+ color: #dc3545;
161
+ border-color: #dc3545;
162
+ }
163
+
164
+ .setting-btn-danger:hover {
165
+ background-color: #dc3545;
166
+ color: #fff;
167
+ }
168
+
169
+ /* === History Dialog === */
170
+ .history-dialog {
171
+ max-width: 600px;
172
+ max-height: 85vh;
173
+ }
174
+
175
+ .history-tab-buttons {
176
+ display: flex;
177
+ gap: 4px;
178
+ margin-left: auto;
179
+ margin-right: 12px;
180
+ }
181
+
182
+ .history-tab {
183
+ padding: 3px 10px;
184
+ font-size: 0.78rem;
185
+ border: 1px solid rgba(255,255,255,0.4);
186
+ border-radius: 12px;
187
+ background: transparent;
188
+ color: rgba(255,255,255,0.7);
189
+ cursor: pointer;
190
+ transition: background-color 0.2s, color 0.2s;
191
+ }
192
+
193
+ .history-tab.active {
194
+ background: rgba(255,255,255,0.25);
195
+ color: #fff;
196
+ }
197
+
198
+ .history-list {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 8px;
202
+ }
203
+
204
+ .history-item {
205
+ padding: 8px 10px;
206
+ border-radius: 8px;
207
+ border: 1px solid #e0e0e0;
208
+ }
209
+
210
+ .history-item-user {
211
+ background-color: #e3f2fd;
212
+ }
213
+
214
+ .history-item-assistant {
215
+ background-color: #f5f5f5;
216
+ }
217
+
218
+ .history-meta {
219
+ display: block;
220
+ font-size: 0.72rem;
221
+ color: #888;
222
+ margin-bottom: 3px;
223
+ }
224
+
225
+ .history-content {
226
+ display: block;
227
+ font-size: 0.85rem;
228
+ color: #333;
229
+ white-space: pre-wrap;
230
+ word-wrap: break-word;
231
+ max-height: 80px;
232
+ overflow: hidden;
233
+ text-overflow: ellipsis;
234
+ }
235
+
236
+ .history-empty {
237
+ text-align: center;
238
+ color: #999;
239
+ font-size: 0.88rem;
240
+ padding: 20px 0;
241
+ }
242
+
243
+ .history-paging {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ gap: 10px;
248
+ padding: 10px 0 0;
249
+ }
250
+
251
+ .history-page-info {
252
+ font-size: 0.78rem;
253
+ color: #888;
254
+ }
255
+
256
+ .history-page-btn {
257
+ padding: 4px 12px;
258
+ font-size: 0.8rem;
259
+ border: 1px solid #ccc;
260
+ border-radius: 4px;
261
+ background: #fff;
262
+ cursor: pointer;
263
+ }
264
+
265
+ .history-page-btn:hover {
266
+ background-color: #f0f0f0;
267
+ }
268
+
269
+ #language-selector option {
270
+ color: #333333;
271
+ background-color: #ffffff;
272
+ }
273
+
274
+ /* === Slide Panel (Rules & Macros) === */
275
+ .slide-panel {
276
+ border-bottom: 1px solid #e0e0e0;
277
+ background-color: #f8f9fa;
278
+ max-height: 280px;
279
+ display: flex;
280
+ flex-direction: column;
281
+ animation: slideDown 0.2s ease-out;
282
+ overflow: hidden; /* Prevent overflow from entire panel */
283
+ }
284
+
285
+ @keyframes slideDown {
286
+ from { max-height: 0; opacity: 0; }
287
+ to { max-height: 280px; opacity: 1; }
288
+ }
289
+
290
+ .slide-panel-header {
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: space-between;
294
+ padding: 10px 16px;
295
+ background-color: #eef2f7;
296
+ border-bottom: 1px solid #dde3ea;
297
+ flex-shrink: 0; /* Prevent header from shrinking */
298
+ }
299
+
300
+ .slide-panel-body {
301
+ flex: 1;
302
+ overflow-y: auto; /* Allow scrolling in body */
303
+ padding: 12px 16px;
304
+ }
305
+ flex-shrink: 0;
306
+ }
307
+
308
+ .slide-panel-header h3 {
309
+ font-size: 0.88rem;
310
+ font-weight: 600;
311
+ color: #4a90d9;
312
+ margin: 0;
313
+ }
314
+
315
+ .slide-panel-close {
316
+ background: none;
317
+ border: none;
318
+ font-size: 1.2rem;
319
+ color: #888888;
320
+ cursor: pointer;
321
+ padding: 0 4px;
322
+ line-height: 1;
323
+ border-radius: 4px;
324
+ transition: color 0.2s, background-color 0.2s;
325
+ }
326
+
327
+ .slide-panel-close:hover {
328
+ color: #333333;
329
+ background-color: rgba(0, 0, 0, 0.08);
330
+ }
331
+
332
+ /* === Rules List === */
333
+ .rules-list {
334
+ list-style: none;
335
+ padding: 0;
336
+ margin: 0;
337
+ display: flex;
338
+ flex-wrap: wrap;
339
+ gap: 6px;
340
+ }
341
+
342
+ .rules-list li {
343
+ display: inline-block;
344
+ padding: 4px 10px;
345
+ font-size: 0.82rem;
346
+ color: #4a6785;
347
+ background-color: #e8eef5;
348
+ border-radius: 12px;
349
+ border: 1px solid #d0dae6;
350
+ cursor: default;
351
+ transition: background-color 0.15s;
352
+ }
353
+
354
+ .rules-list li:hover {
355
+ background-color: #dce4ee;
356
+ }
357
+
358
+ /* === Macros List === */
359
+ .macros-list {
360
+ list-style: none;
361
+ padding: 0;
362
+ margin: 0;
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 8px;
366
+ }
367
+
368
+ .macro-item {
369
+ padding: 10px 12px;
370
+ background-color: #ffffff;
371
+ border-radius: 8px;
372
+ border: 1px solid #e0e5ec;
373
+ transition: box-shadow 0.15s;
374
+ }
375
+
376
+ .macro-item:hover {
377
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
378
+ }
379
+
380
+ .macro-item-header {
381
+ display: flex;
382
+ align-items: center;
383
+ justify-content: space-between;
384
+ margin-bottom: 4px;
385
+ }
386
+
387
+ .macro-item strong {
388
+ font-size: 0.85rem;
389
+ color: #333333;
390
+ }
391
+
392
+ .macro-item-badge {
393
+ display: inline-block;
394
+ font-size: 0.68rem;
395
+ padding: 1px 6px;
396
+ border-radius: 8px;
397
+ background-color: #d4edda;
398
+ color: #28a745;
399
+ font-weight: 500;
400
+ }
401
+
402
+ .macro-item p {
403
+ font-size: 0.8rem;
404
+ color: #666666;
405
+ margin: 2px 0 6px;
406
+ line-height: 1.4;
407
+ }
408
+
409
+ .macro-call-syntax {
410
+ display: inline-block;
411
+ font-family: 'Courier New', Courier, monospace;
412
+ font-size: 0.76rem;
413
+ background-color: #f0f0f0;
414
+ color: #c7254e;
415
+ padding: 3px 8px;
416
+ border-radius: 4px;
417
+ border: 1px solid #e0e0e0;
418
+ }
419
+
420
+ /* Adapter disabled state */
421
+ .macro-item.disabled {
422
+ opacity: 0.5;
423
+ }
424
+
425
+ .macro-item.disabled .macro-item-desc,
426
+ .macro-item.disabled .macro-call-syntax {
427
+ pointer-events: none;
428
+ }
429
+
430
+ /* Adapter toggle */
431
+ .macro-toggle-label {
432
+ margin-left: auto;
433
+ display: flex;
434
+ align-items: center;
435
+ cursor: pointer;
436
+ }
437
+
438
+ .macro-toggle-input {
439
+ width: 16px;
440
+ height: 16px;
441
+ cursor: pointer;
442
+ accent-color: #4a90e2;
443
+ }
444
+
445
+ /* === Message Display === */
446
+ .message-display {
447
+ flex: 1;
448
+ overflow-y: auto;
449
+ padding: 16px;
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: 10px;
453
+ }
454
+
455
+ /* === Messages === */
456
+ .message {
457
+ max-width: 80%;
458
+ padding: 10px 14px;
459
+ border-radius: 12px;
460
+ word-wrap: break-word;
461
+ line-height: 1.45;
462
+ font-size: 0.95rem;
463
+ }
464
+
465
+ .message.user {
466
+ align-self: flex-end;
467
+ background-color: #4a90d9;
468
+ color: #ffffff;
469
+ border-bottom-right-radius: 4px;
470
+ }
471
+
472
+ .message.bot {
473
+ align-self: flex-start;
474
+ background-color: #e9ecef;
475
+ color: #333333;
476
+ border-bottom-left-radius: 4px;
477
+ }
478
+
479
+ /* === Confidence === */
480
+ .confidence {
481
+ display: block;
482
+ font-size: 0.72rem;
483
+ margin-top: 4px;
484
+ font-weight: 500;
485
+ }
486
+
487
+ .confidence-high {
488
+ color: #28a745;
489
+ }
490
+
491
+ .confidence-low {
492
+ color: #dc3545;
493
+ }
494
+
495
+ /* === Adapter Path Breadcrumb === */
496
+ .adapter-path {
497
+ display: block;
498
+ font-size: 0.7rem;
499
+ margin-top: 2px;
500
+ color: #8e8e8e;
501
+ font-style: italic;
502
+ }
503
+
504
+ /* === Response Time === */
505
+ .response-time {
506
+ display: block;
507
+ font-size: 0.68rem;
508
+ margin-top: 2px;
509
+ color: #aaaaaa;
510
+ }
511
+
512
+ /* === Links in bot messages === */
513
+ .message.bot a {
514
+ color: #4a90d9;
515
+ text-decoration: underline;
516
+ word-break: break-all;
517
+ }
518
+
519
+ .message.bot a:hover {
520
+ color: #3a7bc8;
521
+ }
522
+
523
+ /* === Loading Indicator === */
524
+ .loading-indicator {
525
+ align-self: flex-start;
526
+ padding: 10px 14px;
527
+ background-color: #e9ecef;
528
+ border-radius: 12px;
529
+ font-size: 0.9rem;
530
+ color: #888888;
531
+ animation: pulse 1.2s ease-in-out infinite;
532
+ }
533
+
534
+ @keyframes pulse {
535
+ 0%, 100% { opacity: 1; }
536
+ 50% { opacity: 0.5; }
537
+ }
538
+
539
+ /* === Input Area === */
540
+ .input-area {
541
+ display: flex;
542
+ padding: 12px 16px;
543
+ border-top: 1px solid #e0e0e0;
544
+ background-color: #ffffff;
545
+ gap: 8px;
546
+ }
547
+
548
+ .input-area input {
549
+ flex: 1;
550
+ padding: 10px 14px;
551
+ border: 1px solid #cccccc;
552
+ border-radius: 20px;
553
+ font-size: 0.95rem;
554
+ outline: none;
555
+ transition: border-color 0.2s;
556
+ }
557
+
558
+ .input-area input:focus {
559
+ border-color: #4a90d9;
560
+ }
561
+
562
+ .input-area button {
563
+ padding: 10px 20px;
564
+ background-color: #4a90d9;
565
+ color: #ffffff;
566
+ border: none;
567
+ border-radius: 20px;
568
+ font-size: 0.95rem;
569
+ cursor: pointer;
570
+ transition: background-color 0.2s;
571
+ white-space: nowrap;
572
+ }
573
+
574
+ .input-area button:hover {
575
+ background-color: #3a7bc8;
576
+ }
577
+
578
+ .input-area button:disabled,
579
+ .input-area button.disabled {
580
+ background-color: #a0bfdf;
581
+ cursor: not-allowed;
582
+ opacity: 0.7;
583
+ }
584
+
585
+ .input-area button:disabled:hover,
586
+ .input-area button.disabled:hover {
587
+ background-color: #a0bfdf;
588
+ }
589
+
590
+ /* === LLM Loading Status === */
591
+ .llm-loading-status {
592
+ align-self: flex-start;
593
+ padding: 10px 14px;
594
+ background-color: #fff3cd;
595
+ border: 1px solid #ffc107;
596
+ border-radius: 12px;
597
+ font-size: 0.88rem;
598
+ color: #856404;
599
+ animation: pulse 1.5s ease-in-out infinite;
600
+ }
601
+
602
+ /* === Attachment Preview === */
603
+ .attachment-preview {
604
+ display: flex;
605
+ align-items: center;
606
+ gap: 8px;
607
+ padding: 6px 16px;
608
+ background-color: #f8f9fa;
609
+ border-top: 1px solid #e0e0e0;
610
+ }
611
+
612
+ .attachment-thumb {
613
+ width: 40px;
614
+ height: 40px;
615
+ object-fit: cover;
616
+ border-radius: 6px;
617
+ border: 1px solid #ddd;
618
+ }
619
+
620
+ .attachment-name {
621
+ flex: 1;
622
+ font-size: 0.82rem;
623
+ color: #555;
624
+ overflow: hidden;
625
+ text-overflow: ellipsis;
626
+ white-space: nowrap;
627
+ }
628
+
629
+ .attachment-remove {
630
+ background: none;
631
+ border: none;
632
+ font-size: 1.2rem;
633
+ color: #999;
634
+ cursor: pointer;
635
+ padding: 2px 6px;
636
+ border-radius: 4px;
637
+ line-height: 1;
638
+ transition: color 0.2s, background-color 0.2s;
639
+ }
640
+
641
+ .attachment-remove:hover {
642
+ color: #dc3545;
643
+ background-color: rgba(220, 53, 69, 0.1);
644
+ }
645
+
646
+ /* === Attach Button === */
647
+ #attach-button {
648
+ background: none;
649
+ border: none;
650
+ font-size: 1.3rem;
651
+ cursor: pointer;
652
+ padding: 6px;
653
+ border-radius: 50%;
654
+ transition: background-color 0.2s;
655
+ flex-shrink: 0;
656
+ }
657
+
658
+ #attach-button:hover {
659
+ background-color: #e9ecef;
660
+ }
661
+
662
+ /* === Message Image === */
663
+ .message-image {
664
+ display: block;
665
+ max-width: 100%;
666
+ max-height: 200px;
667
+ border-radius: 8px;
668
+ margin-bottom: 6px;
669
+ object-fit: contain;
670
+ }
671
+
672
+ /* === LLM Cancel Button === */
673
+ .llm-cancel-container {
674
+ align-self: flex-start;
675
+ margin-top: 4px;
676
+ }
677
+
678
+ .llm-cancel-button {
679
+ display: inline-block;
680
+ padding: 5px 14px;
681
+ font-size: 0.82rem;
682
+ color: #dc3545;
683
+ background-color: #fff;
684
+ border: 1px solid #dc3545;
685
+ border-radius: 16px;
686
+ cursor: pointer;
687
+ transition: background-color 0.2s, color 0.2s;
688
+ }
689
+
690
+ .llm-cancel-button:hover {
691
+ background-color: #dc3545;
692
+ color: #fff;
693
+ }
694
+
695
+ /* === Streaming Message === */
696
+ .message-text {
697
+ white-space: pre-wrap;
698
+ word-wrap: break-word;
699
+ }
700
+
701
+ .message.bot.streaming .message-text {
702
+ border-right: 2px solid #4a90d9;
703
+ padding-right: 2px;
704
+ animation: blink-cursor 0.8s step-end infinite;
705
+ }
706
+
707
+ @keyframes blink-cursor {
708
+ 50% { border-color: transparent; }
709
+ }
710
+
711
+ /* === LLM Thinking Block === */
712
+ .llm-thinking-block {
713
+ background-color: #e8f5e9;
714
+ border-left: 3px solid #81c784;
715
+ border-radius: 6px;
716
+ padding: 8px 10px;
717
+ margin-bottom: 8px;
718
+ font-size: 0.84rem;
719
+ line-height: 1.5;
720
+ color: #2e7d32;
721
+ }
722
+
723
+ .llm-thinking-label {
724
+ display: block;
725
+ font-weight: 600;
726
+ font-size: 0.78rem;
727
+ margin-bottom: 4px;
728
+ color: #388e3c;
729
+ }
730
+
731
+ .llm-thinking-content {
732
+ display: block;
733
+ white-space: pre-wrap;
734
+ word-wrap: break-word;
735
+ color: #1b5e20;
736
+ }
737
+
738
+ /* === Utility === */
739
+ .hidden {
740
+ display: none !important;
741
+ }
742
+
743
+ /* === Responsive — Mobile (max-width: 768px, min-width: 320px) === */
744
+ @media (max-width: 768px) {
745
+ body {
746
+ padding: 0;
747
+ align-items: stretch;
748
+ }
749
+
750
+ .chat-container {
751
+ max-width: 100%;
752
+ height: 100vh;
753
+ border-radius: 0;
754
+ box-shadow: none;
755
+ }
756
+
757
+ .chat-header {
758
+ padding: 10px 12px;
759
+ }
760
+
761
+ .chat-header h1 {
762
+ font-size: 1.1rem;
763
+ }
764
+
765
+ .header-controls {
766
+ gap: 6px;
767
+ }
768
+
769
+ #language-selector {
770
+ font-size: 0.8rem;
771
+ padding: 3px 6px;
772
+ }
773
+
774
+ .message-display {
775
+ padding: 12px;
776
+ }
777
+
778
+ .message {
779
+ max-width: 88%;
780
+ font-size: 0.9rem;
781
+ }
782
+
783
+ .input-area {
784
+ padding: 10px 12px;
785
+ }
786
+
787
+ .input-area input {
788
+ padding: 8px 12px;
789
+ font-size: 0.9rem;
790
+ }
791
+
792
+ .input-area button {
793
+ padding: 8px 16px;
794
+ font-size: 0.9rem;
795
+ }
796
+
797
+ .rules-panel,
798
+ .macros-panel,
799
+ .slide-panel {
800
+ max-height: 200px;
801
+ }
802
+ }
803
+
804
+ /* === Help Dialog Overlay === */
805
+ .help-overlay {
806
+ position: fixed;
807
+ top: 0;
808
+ left: 0;
809
+ width: 100%;
810
+ height: 100%;
811
+ background-color: rgba(0, 0, 0, 0.5);
812
+ display: flex;
813
+ justify-content: center;
814
+ align-items: center;
815
+ z-index: 1000;
816
+ animation: fadeIn 0.2s ease-out;
817
+ }
818
+
819
+ @keyframes fadeIn {
820
+ from { opacity: 0; }
821
+ to { opacity: 1; }
822
+ }
823
+
824
+ .help-dialog {
825
+ background-color: #ffffff;
826
+ border-radius: 12px;
827
+ width: 90%;
828
+ max-width: 520px;
829
+ max-height: 80vh;
830
+ display: flex;
831
+ flex-direction: column;
832
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
833
+ animation: slideUp 0.25s ease-out;
834
+ }
835
+
836
+ @keyframes slideUp {
837
+ from { transform: translateY(24px); opacity: 0; }
838
+ to { transform: translateY(0); opacity: 1; }
839
+ }
840
+
841
+ .help-dialog-header {
842
+ display: flex;
843
+ align-items: center;
844
+ justify-content: space-between;
845
+ padding: 16px 20px;
846
+ border-bottom: 1px solid #e0e0e0;
847
+ background: linear-gradient(135deg, #4a90d9, #5ba3e6);
848
+ color: #ffffff;
849
+ border-radius: 12px 12px 0 0;
850
+ }
851
+
852
+ .help-dialog-header h2 {
853
+ font-size: 1.05rem;
854
+ font-weight: 600;
855
+ margin: 0;
856
+ }
857
+
858
+ .help-close-button {
859
+ background: none;
860
+ border: none;
861
+ color: #ffffff;
862
+ font-size: 1.5rem;
863
+ cursor: pointer;
864
+ padding: 0 4px;
865
+ line-height: 1;
866
+ border-radius: 4px;
867
+ transition: background-color 0.2s;
868
+ }
869
+
870
+ .help-close-button:hover {
871
+ background-color: rgba(255, 255, 255, 0.2);
872
+ }
873
+
874
+ .help-dialog-body {
875
+ padding: 20px;
876
+ overflow-y: auto;
877
+ font-size: 0.9rem;
878
+ line-height: 1.6;
879
+ color: #333333;
880
+ }
881
+
882
+ .help-section {
883
+ margin-bottom: 18px;
884
+ padding-bottom: 16px;
885
+ border-bottom: 1px solid #f0f0f0;
886
+ }
887
+
888
+ .help-section:last-child {
889
+ margin-bottom: 0;
890
+ padding-bottom: 0;
891
+ border-bottom: none;
892
+ }
893
+
894
+ .help-section h3 {
895
+ font-size: 0.92rem;
896
+ font-weight: 600;
897
+ color: #4a90d9;
898
+ margin-bottom: 8px;
899
+ display: flex;
900
+ align-items: center;
901
+ gap: 6px;
902
+ }
903
+
904
+ .help-section p {
905
+ margin: 4px 0;
906
+ color: #555555;
907
+ }
908
+
909
+ .help-section ul {
910
+ list-style: none;
911
+ padding: 0;
912
+ margin: 6px 0 0 0;
913
+ }
914
+
915
+ .help-section li {
916
+ padding: 4px 0 4px 16px;
917
+ position: relative;
918
+ color: #555555;
919
+ }
920
+
921
+ .help-section li::before {
922
+ content: '•';
923
+ position: absolute;
924
+ left: 4px;
925
+ color: #4a90d9;
926
+ font-weight: bold;
927
+ }
928
+
929
+ .help-example {
930
+ display: inline-block;
931
+ background-color: #f0f4f8;
932
+ color: #4a90d9;
933
+ padding: 2px 8px;
934
+ border-radius: 4px;
935
+ font-size: 0.84rem;
936
+ font-family: 'Courier New', Courier, monospace;
937
+ }
938
+
939
+ .help-confidence-demo {
940
+ display: flex;
941
+ gap: 12px;
942
+ margin-top: 8px;
943
+ }
944
+
945
+ .help-confidence-item {
946
+ display: flex;
947
+ align-items: center;
948
+ gap: 6px;
949
+ font-size: 0.84rem;
950
+ }
951
+
952
+ .help-dot {
953
+ width: 10px;
954
+ height: 10px;
955
+ border-radius: 50%;
956
+ display: inline-block;
957
+ }
958
+
959
+ .help-dot.green { background-color: #28a745; }
960
+ .help-dot.red { background-color: #dc3545; }
961
+
962
+ /* === Responsive — Help Dialog Mobile === */
963
+ @media (max-width: 768px) {
964
+ .help-dialog {
965
+ width: 95%;
966
+ max-height: 85vh;
967
+ }
968
+
969
+ .help-dialog-header {
970
+ padding: 12px 16px;
971
+ }
972
+
973
+ .help-dialog-body {
974
+ padding: 16px;
975
+ }
976
+ }
977
+
978
+ /* === Interaction Mode Badge === */
979
+ .interaction-mode-badge {
980
+ font-size: 0.85rem;
981
+ padding: 2px 6px;
982
+ background-color: rgba(255, 255, 255, 0.2);
983
+ border-radius: 10px;
984
+ cursor: pointer; /* Clickable */
985
+ user-select: none;
986
+ white-space: nowrap;
987
+ transition: background-color 0.2s ease, transform 0.1s ease;
988
+ }
989
+
990
+ .interaction-mode-badge:hover {
991
+ background-color: rgba(255, 255, 255, 0.35);
992
+ transform: scale(1.05);
993
+ }
994
+
995
+ .interaction-mode-badge:active {
996
+ transform: scale(0.95);
997
+ }
998
+
999
+ /* === Adapter Prefix Badge === */
1000
+ .adapter-prefix-badge {
1001
+ display: inline-block;
1002
+ font-size: 0.8rem;
1003
+ padding: 4px 10px;
1004
+ margin-right: 8px;
1005
+ background-color: #e3f2fd;
1006
+ color: #1976d2;
1007
+ border: 1px solid #90caf9;
1008
+ border-radius: 12px;
1009
+ white-space: nowrap;
1010
+ transition: opacity 0.2s;
1011
+ }
1012
+
1013
+ .adapter-prefix-badge.hidden {
1014
+ display: none;
1015
+ }
1016
+
1017
+ /* === Voice Input Button === */
1018
+ #voice-input-button {
1019
+ background: none;
1020
+ border: none;
1021
+ font-size: 1.2rem;
1022
+ cursor: pointer;
1023
+ padding: 6px 10px;
1024
+ border-radius: 6px;
1025
+ transition: background-color 0.2s, transform 0.1s;
1026
+ flex-shrink: 0;
1027
+ }
1028
+
1029
+ #voice-input-button:hover {
1030
+ background-color: rgba(74, 144, 217, 0.1);
1031
+ }
1032
+
1033
+ /* Animation pulse khi đang lắng nghe */
1034
+ #voice-input-button.voice-listening {
1035
+ animation: voice-pulse 1s ease-in-out infinite;
1036
+ color: #dc3545;
1037
+ }
1038
+
1039
+ @keyframes voice-pulse {
1040
+ 0%, 100% { transform: scale(1); opacity: 1; }
1041
+ 50% { transform: scale(1.2); opacity: 0.7; }
1042
+ }
1043
+
1044
+ /* === Settings Panel — Voice Controls === */
1045
+ .setting-divider {
1046
+ border: none;
1047
+ border-top: 1px solid #e9ecef;
1048
+ margin: 12px 0;
1049
+ }
1050
+
1051
+ .setting-label {
1052
+ display: block;
1053
+ font-size: 0.85rem;
1054
+ color: #555;
1055
+ margin-bottom: 4px;
1056
+ }
1057
+
1058
+ .setting-select {
1059
+ width: 100%;
1060
+ padding: 6px 8px;
1061
+ border: 1px solid #ced4da;
1062
+ border-radius: 6px;
1063
+ font-size: 0.85rem;
1064
+ background-color: #fff;
1065
+ cursor: pointer;
1066
+ }
1067
+
1068
+ .setting-select:focus {
1069
+ outline: none;
1070
+ border-color: #4a90d9;
1071
+ box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.2);
1072
+ }
1073
+
1074
+ /* === History Attachment Thumbnail === */
1075
+ .history-attachment-thumb {
1076
+ display: block;
1077
+ max-width: 80px;
1078
+ max-height: 60px;
1079
+ border-radius: 4px;
1080
+ margin-top: 4px;
1081
+ object-fit: cover;
1082
+ border: 1px solid #dee2e6;
1083
+ }