Spaces:
Running
Running
Upload 26 files
Browse files- adapters/adapter-registry.js +69 -61
- adapters/best-match.js +58 -58
- adapters/llm-adapter.js +461 -460
- adapters/logic-dispatcher.js +71 -64
- adapters/math-adapter.js +77 -77
- adapters/specific-response.js +53 -53
- adapters/text-similarity.js +427 -427
- adapters/time-adapter.js +89 -89
- adapters/unit-conversion.js +145 -145
- adapters/voice-adapter.js +224 -0
- adapters/web-search.js +207 -207
- app.js +0 -0
- brain.js +62 -62
- brain/en.rive +220 -220
- brain/ja.rive +142 -142
- brain/vi.rive +245 -245
- data-loader.js +46 -46
- data/adapter-registry.json +82 -82
- data/chat-history-db.js +390 -165
- data/help-content.json +134 -134
- data/preprocessed.json +0 -0
- data/qa-dataset.json +44 -44
- data/specific-responses.json +23 -23
- index.html +186 -133
- scripts/preprocess.js +280 -280
- style.css +1083 -948
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 (
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 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: '申し訳ありませんが
|
| 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 |
-
|
| 76 |
-
*
|
| 77 |
-
*
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
*
|
| 85 |
-
*
|
| 86 |
-
* @param {
|
| 87 |
-
* @param {
|
| 88 |
-
* @
|
| 89 |
-
*
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
*
|
| 120 |
-
*
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
*
|
| 128 |
-
*
|
| 129 |
-
*
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
if (
|
| 133 |
-
if (
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
*
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
*
|
| 221 |
-
*
|
| 222 |
-
* @param {
|
| 223 |
-
* @param {
|
| 224 |
-
* @
|
| 225 |
-
*
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
var
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
*
|
| 278 |
-
*
|
| 279 |
-
* @param {string}
|
| 280 |
-
* @param {
|
| 281 |
-
* @param {
|
| 282 |
-
* @
|
| 283 |
-
*
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
var
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
var
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
*
|
| 340 |
-
*
|
| 341 |
-
* @param {
|
| 342 |
-
* @param {string
|
| 343 |
-
* @param {
|
| 344 |
-
* @param {
|
| 345 |
-
* @
|
| 346 |
-
*
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
var
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
if (lang === '
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
else
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
*
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
*
|
| 408 |
-
*
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
*
|
| 416 |
-
*
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
*
|
| 424 |
-
*
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
*
|
| 432 |
-
*
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
globalThis.
|
| 446 |
-
globalThis.
|
| 447 |
-
globalThis.
|
| 448 |
-
globalThis.
|
| 449 |
-
globalThis.
|
| 450 |
-
globalThis.
|
| 451 |
-
globalThis.
|
| 452 |
-
globalThis.
|
| 453 |
-
globalThis.
|
| 454 |
-
globalThis.
|
| 455 |
-
globalThis.
|
| 456 |
-
globalThis.
|
| 457 |
-
globalThis.
|
| 458 |
-
globalThis.
|
| 459 |
-
globalThis.
|
| 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 có
|
| 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 và 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 |
+
* Có thể dùng như subroutine hoặc gọi trực tiếp.
|
| 341 |
+
* @param {object} rs - RiveScript instance (có 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 mà 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ỉ có ảnh mà 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
|
| 28 |
-
specificResponseAdapter,
|
| 29 |
-
timeAdapter,
|
| 30 |
-
mathematicalEvaluationAdapter,
|
| 31 |
-
unitConversionAdapter,
|
| 32 |
-
bestMatchAdapter
|
| 33 |
-
];
|
| 34 |
-
|
| 35 |
-
var pathLenBefore = _adapterPath.length;
|
| 36 |
-
|
| 37 |
-
for (var i = 0; i <
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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ị
|
| 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 =
|
| 8 |
-
var CHAT_HISTORY_STORE_NAME = 'messages';
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
*
|
| 13 |
-
*
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 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 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
*
|
| 85 |
-
*
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
var
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
};
|
| 124 |
-
|
| 125 |
-
});
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
/**
|
| 129 |
-
*
|
| 130 |
-
* @
|
| 131 |
-
*
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
var
|
| 137 |
-
var
|
| 138 |
-
|
| 139 |
-
request
|
| 140 |
-
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
*
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 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>Đỏ (<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 (<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>赤(<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>Đỏ (<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 (<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>赤(<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 |
-
<
|
| 16 |
-
|
| 17 |
-
<
|
| 18 |
-
|
| 19 |
-
<
|
| 20 |
-
<button id="
|
| 21 |
-
<button id="
|
| 22 |
-
<button id="
|
| 23 |
-
<
|
| 24 |
-
|
| 25 |
-
<option value="
|
| 26 |
-
<option value="
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
<
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
<
|
| 34 |
-
|
| 35 |
-
<div
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
<div
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
<
|
| 43 |
-
|
| 44 |
-
<div
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
<div
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
<
|
| 52 |
-
|
| 53 |
-
<div
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
<div
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
<
|
| 66 |
-
|
| 67 |
-
<div
|
| 68 |
-
|
| 69 |
-
<button id="
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
<
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
<
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
<
|
| 133 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">×</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">×</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">×</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">×</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">×</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">×</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 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
font-size:
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
color: #
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
.
|
| 492 |
-
color: #
|
| 493 |
-
}
|
| 494 |
-
|
| 495 |
-
/* ===
|
| 496 |
-
.
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
font-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
.input-area
|
| 559 |
-
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
border:
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
}
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
background-color: #
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
padding:
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
font-size:
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
padding:
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
}
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
.
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
color: #
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
}
|
| 749 |
-
|
| 750 |
-
.
|
| 751 |
-
max-width:
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
}
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
}
|
| 823 |
-
|
| 824 |
-
.help-dialog
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
margin
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
color: #4a90d9;
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 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 |
+
}
|