Spaces:
Running
Running
Upload 23 files
Browse files- .gitignore +143 -0
- adapters/adapter-registry.js +59 -0
- adapters/best-match.js +58 -0
- adapters/logic-dispatcher.js +59 -0
- adapters/math-adapter.js +77 -0
- adapters/specific-response.js +53 -0
- adapters/text-similarity.js +427 -0
- adapters/time-adapter.js +89 -0
- adapters/unit-conversion.js +145 -0
- adapters/web-search.js +207 -0
- app.js +1108 -0
- brain.js +62 -0
- brain/en.rive +220 -0
- brain/ja.rive +142 -0
- brain/vi.rive +245 -0
- data-loader.js +46 -0
- data/adapter-registry.json +72 -0
- data/help-content.json +134 -0
- data/preprocessed.json +0 -0
- data/qa-dataset.json +44 -0
- data/specific-responses.json +23 -0
- index.html +76 -19
- style.css +595 -28
.gitignore
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
lerna-debug.log*
|
| 8 |
+
|
| 9 |
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
| 10 |
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
| 11 |
+
|
| 12 |
+
# Runtime data
|
| 13 |
+
pids
|
| 14 |
+
*.pid
|
| 15 |
+
*.seed
|
| 16 |
+
*.pid.lock
|
| 17 |
+
|
| 18 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
| 19 |
+
lib-cov
|
| 20 |
+
|
| 21 |
+
# Coverage directory used by tools like istanbul
|
| 22 |
+
coverage
|
| 23 |
+
*.lcov
|
| 24 |
+
|
| 25 |
+
# nyc test coverage
|
| 26 |
+
.nyc_output
|
| 27 |
+
|
| 28 |
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
| 29 |
+
.grunt
|
| 30 |
+
|
| 31 |
+
# Bower dependency directory (https://bower.io/)
|
| 32 |
+
bower_components
|
| 33 |
+
|
| 34 |
+
# node-waf configuration
|
| 35 |
+
.lock-wscript
|
| 36 |
+
|
| 37 |
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
| 38 |
+
build/Release
|
| 39 |
+
|
| 40 |
+
# Dependency directories
|
| 41 |
+
node_modules/
|
| 42 |
+
jspm_packages/
|
| 43 |
+
|
| 44 |
+
# Snowpack dependency directory (https://snowpack.dev/)
|
| 45 |
+
web_modules/
|
| 46 |
+
|
| 47 |
+
# TypeScript cache
|
| 48 |
+
*.tsbuildinfo
|
| 49 |
+
|
| 50 |
+
# Optional npm cache directory
|
| 51 |
+
.npm
|
| 52 |
+
|
| 53 |
+
# Optional eslint cache
|
| 54 |
+
.eslintcache
|
| 55 |
+
|
| 56 |
+
# Optional stylelint cache
|
| 57 |
+
.stylelintcache
|
| 58 |
+
|
| 59 |
+
# Optional REPL history
|
| 60 |
+
.node_repl_history
|
| 61 |
+
|
| 62 |
+
# Output of 'npm pack'
|
| 63 |
+
*.tgz
|
| 64 |
+
|
| 65 |
+
# Yarn Integrity file
|
| 66 |
+
.yarn-integrity
|
| 67 |
+
|
| 68 |
+
# dotenv environment variable files
|
| 69 |
+
.env
|
| 70 |
+
.env.*
|
| 71 |
+
!.env.example
|
| 72 |
+
|
| 73 |
+
# parcel-bundler cache (https://parceljs.org/)
|
| 74 |
+
.cache
|
| 75 |
+
.parcel-cache
|
| 76 |
+
|
| 77 |
+
# Next.js build output
|
| 78 |
+
.next
|
| 79 |
+
out
|
| 80 |
+
|
| 81 |
+
# Nuxt.js build / generate output
|
| 82 |
+
.nuxt
|
| 83 |
+
dist
|
| 84 |
+
.output
|
| 85 |
+
|
| 86 |
+
# Gatsby files
|
| 87 |
+
.cache/
|
| 88 |
+
# Comment in the public line in if your project uses Gatsby and not Next.js
|
| 89 |
+
# https://nextjs.org/blog/next-9-1#public-directory-support
|
| 90 |
+
# public
|
| 91 |
+
|
| 92 |
+
# vuepress build output
|
| 93 |
+
.vuepress/dist
|
| 94 |
+
|
| 95 |
+
# vuepress v2.x temp directory
|
| 96 |
+
.temp
|
| 97 |
+
|
| 98 |
+
# Sveltekit cache directory
|
| 99 |
+
.svelte-kit/
|
| 100 |
+
|
| 101 |
+
# vitepress build output
|
| 102 |
+
**/.vitepress/dist
|
| 103 |
+
|
| 104 |
+
# vitepress cache directory
|
| 105 |
+
**/.vitepress/cache
|
| 106 |
+
|
| 107 |
+
# Docusaurus cache and generated files
|
| 108 |
+
.docusaurus
|
| 109 |
+
|
| 110 |
+
# Serverless directories
|
| 111 |
+
.serverless/
|
| 112 |
+
|
| 113 |
+
# FuseBox cache
|
| 114 |
+
.fusebox/
|
| 115 |
+
|
| 116 |
+
# DynamoDB Local files
|
| 117 |
+
.dynamodb/
|
| 118 |
+
|
| 119 |
+
# Firebase cache directory
|
| 120 |
+
.firebase/
|
| 121 |
+
|
| 122 |
+
# TernJS port file
|
| 123 |
+
.tern-port
|
| 124 |
+
|
| 125 |
+
# Stores VSCode versions used for testing VSCode extensions
|
| 126 |
+
.vscode-test
|
| 127 |
+
|
| 128 |
+
# pnpm
|
| 129 |
+
.pnpm-store
|
| 130 |
+
|
| 131 |
+
# yarn v3
|
| 132 |
+
.pnp.*
|
| 133 |
+
.yarn/*
|
| 134 |
+
!.yarn/patches
|
| 135 |
+
!.yarn/plugins
|
| 136 |
+
!.yarn/releases
|
| 137 |
+
!.yarn/sdks
|
| 138 |
+
!.yarn/versions
|
| 139 |
+
|
| 140 |
+
# Vite files
|
| 141 |
+
vite.config.js.timestamp-*
|
| 142 |
+
vite.config.ts.timestamp-*
|
| 143 |
+
.vite/
|
adapters/adapter-registry.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
};
|
| 19 |
+
|
| 20 |
+
function getAdapterDisplayName(adapterKey) {
|
| 21 |
+
var lang = currentLang || 'vi';
|
| 22 |
+
var names = ADAPTER_DISPLAY_NAMES[adapterKey];
|
| 23 |
+
if (!names) return adapterKey;
|
| 24 |
+
return names[lang] || names['vi'] || adapterKey;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function registerAdapters(bot, lang) {
|
| 28 |
+
var ADAPTER_FUNCTIONS = {
|
| 29 |
+
best_match: bestMatchAdapter,
|
| 30 |
+
logic_adapter: logicAdapterDispatcher,
|
| 31 |
+
mathematical_evaluation: mathematicalEvaluationAdapter,
|
| 32 |
+
specific_response: specificResponseAdapter,
|
| 33 |
+
time_adapter: timeAdapter,
|
| 34 |
+
unit_conversion: unitConversionAdapter,
|
| 35 |
+
web_search: webSearchAdapter
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
var adapterNames = Object.keys(ADAPTER_REGISTRY);
|
| 39 |
+
for (var i = 0; i < adapterNames.length; i++) {
|
| 40 |
+
var name = adapterNames[i];
|
| 41 |
+
var entry = ADAPTER_REGISTRY[name];
|
| 42 |
+
var fn = ADAPTER_FUNCTIONS[name];
|
| 43 |
+
|
| 44 |
+
if (entry.active && typeof fn === 'function') {
|
| 45 |
+
(function (adapterFn) {
|
| 46 |
+
bot.setSubroutine(name, function (rs, args) {
|
| 47 |
+
return adapterFn(rs, args);
|
| 48 |
+
});
|
| 49 |
+
})(fn);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Node/test: export to globalThis
|
| 55 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 56 |
+
globalThis.ADAPTER_DISPLAY_NAMES = ADAPTER_DISPLAY_NAMES;
|
| 57 |
+
globalThis.getAdapterDisplayName = getAdapterDisplayName;
|
| 58 |
+
globalThis.registerAdapters = registerAdapters;
|
| 59 |
+
}
|
adapters/best-match.js
ADDED
|
@@ -0,0 +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.3;
|
| 12 |
+
|
| 13 |
+
if (input.length === 0) {
|
| 14 |
+
if (lang === 'en') return 'Please provide a question to search for.';
|
| 15 |
+
if (lang === 'ja') return '検索する質問を入力してください。';
|
| 16 |
+
return 'Vui lòng nhập câu hỏi để tìm kiếm.';
|
| 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 ppResult.answer;
|
| 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 bestAnswer;
|
| 49 |
+
|
| 50 |
+
if (lang === 'en') return 'Sorry, I could not find a suitable answer for your question.';
|
| 51 |
+
if (lang === 'ja') return '申し訳ありませんが、ご質問に適した回答が見つかりませんでした。';
|
| 52 |
+
return '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.';
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Node/test: export to globalThis
|
| 56 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 57 |
+
globalThis.bestMatchAdapter = bestMatchAdapter;
|
| 58 |
+
}
|
adapters/logic-dispatcher.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// Logic Adapter Dispatcher — Điều phối các adapter con theo ưu tiên
|
| 3 |
+
// Phụ thuộc: currentLang, _adapterPath, specificResponseAdapter, timeAdapter,
|
| 4 |
+
// mathematicalEvaluationAdapter, unitConversionAdapter, bestMatchAdapter
|
| 5 |
+
// ============================================================
|
| 6 |
+
|
| 7 |
+
var INVALID_RESPONSE_PHRASES = [
|
| 8 |
+
'không có phản hồi cụ thể', 'không hiểu', 'không thể', 'không tìm thấy',
|
| 9 |
+
'không tìm được', 'cú pháp không hợp lệ',
|
| 10 |
+
'no specific response', "don't understand", 'cannot', 'not found',
|
| 11 |
+
'invalid syntax', 'could not find',
|
| 12 |
+
'特定の応答はありません', '理解できません', 'できません', '見つかりません',
|
| 13 |
+
'構文が無効', '見つかりませんでした'
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
function isValidAdapterResult(result) {
|
| 17 |
+
if (typeof result !== 'string' || result.trim().length === 0) return false;
|
| 18 |
+
var lower = result.toLowerCase();
|
| 19 |
+
for (var i = 0; i < INVALID_RESPONSE_PHRASES.length; i++) {
|
| 20 |
+
if (lower.indexOf(INVALID_RESPONSE_PHRASES[i].toLowerCase()) !== -1) return false;
|
| 21 |
+
}
|
| 22 |
+
return true;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function logicAdapterDispatcher(rs, args) {
|
| 26 |
+
_adapterPath.push('logic_adapter');
|
| 27 |
+
var adapters = [
|
| 28 |
+
specificResponseAdapter,
|
| 29 |
+
timeAdapter,
|
| 30 |
+
mathematicalEvaluationAdapter,
|
| 31 |
+
unitConversionAdapter,
|
| 32 |
+
bestMatchAdapter
|
| 33 |
+
];
|
| 34 |
+
|
| 35 |
+
var pathLenBefore = _adapterPath.length;
|
| 36 |
+
|
| 37 |
+
for (var i = 0; i < adapters.length; i++) {
|
| 38 |
+
try {
|
| 39 |
+
_adapterPath.length = pathLenBefore;
|
| 40 |
+
var result = adapters[i](rs, args);
|
| 41 |
+
if (isValidAdapterResult(result)) return result;
|
| 42 |
+
} catch (err) {
|
| 43 |
+
continue;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
_adapterPath.length = pathLenBefore;
|
| 48 |
+
var lang = currentLang || 'vi';
|
| 49 |
+
if (lang === 'en') return 'Sorry, I cannot process your request at this time.';
|
| 50 |
+
if (lang === 'ja') return '申し訳ありませんが、現在リクエストを処理できません。';
|
| 51 |
+
return 'Xin lỗi, mình không thể xử lý yêu cầu của bạn lúc này.';
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Node/test: export to globalThis
|
| 55 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 56 |
+
globalThis.INVALID_RESPONSE_PHRASES = INVALID_RESPONSE_PHRASES;
|
| 57 |
+
globalThis.isValidAdapterResult = isValidAdapterResult;
|
| 58 |
+
globalThis.logicAdapterDispatcher = logicAdapterDispatcher;
|
| 59 |
+
}
|
adapters/math-adapter.js
ADDED
|
@@ -0,0 +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 |
+
}
|
adapters/specific-response.js
ADDED
|
@@ -0,0 +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 |
+
}
|
adapters/text-similarity.js
ADDED
|
@@ -0,0 +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 |
+
}
|
adapters/time-adapter.js
ADDED
|
@@ -0,0 +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 |
+
}
|
adapters/unit-conversion.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// Unit Conversion Adapter — Chuyển đổi đơn vị đo lường
|
| 3 |
+
// Phụ thuộc: currentLang, _adapterPath
|
| 4 |
+
// ============================================================
|
| 5 |
+
|
| 6 |
+
var CONVERSION_FACTORS = {
|
| 7 |
+
length: { m: 1, km: 1000, cm: 0.01, mm: 0.001, mile: 1609.344, yard: 0.9144, foot: 0.3048, inch: 0.0254 },
|
| 8 |
+
mass: { kg: 1, g: 0.001, mg: 0.000001, pound: 0.453592, ounce: 0.0283495 }
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
var TEMPERATURE_UNITS = {
|
| 12 |
+
celsius: 'celsius', c: 'celsius',
|
| 13 |
+
fahrenheit: 'fahrenheit', f: 'fahrenheit',
|
| 14 |
+
kelvin: 'kelvin', k: 'kelvin'
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
function convertTemperature(value, from, to) {
|
| 18 |
+
if (from === to) return value;
|
| 19 |
+
var celsius;
|
| 20 |
+
if (from === 'celsius') celsius = value;
|
| 21 |
+
else if (from === 'fahrenheit') celsius = (value - 32) * 5 / 9;
|
| 22 |
+
else celsius = value - 273.15;
|
| 23 |
+
|
| 24 |
+
if (to === 'celsius') return celsius;
|
| 25 |
+
if (to === 'fahrenheit') return (celsius * 9 / 5) + 32;
|
| 26 |
+
return celsius + 273.15;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function convertUnit(value, fromUnit, toUnit) {
|
| 30 |
+
var from = String(fromUnit).toLowerCase().trim();
|
| 31 |
+
var to = String(toUnit).toLowerCase().trim();
|
| 32 |
+
if (from === to) return value;
|
| 33 |
+
|
| 34 |
+
var fromTemp = TEMPERATURE_UNITS[from];
|
| 35 |
+
var toTemp = TEMPERATURE_UNITS[to];
|
| 36 |
+
if (fromTemp && toTemp) return convertTemperature(value, fromTemp, toTemp);
|
| 37 |
+
if (fromTemp || toTemp) return null;
|
| 38 |
+
|
| 39 |
+
var categories = Object.keys(CONVERSION_FACTORS);
|
| 40 |
+
for (var i = 0; i < categories.length; i++) {
|
| 41 |
+
var factors = CONVERSION_FACTORS[categories[i]];
|
| 42 |
+
if (factors[from] !== undefined && factors[to] !== undefined) {
|
| 43 |
+
return value * (factors[from] / factors[to]);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
return null;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function parseConversionRequest(input, lang) {
|
| 50 |
+
if (typeof input !== 'string' || input.trim().length === 0) return null;
|
| 51 |
+
var text = input.trim();
|
| 52 |
+
|
| 53 |
+
var separators;
|
| 54 |
+
if (lang === 'ja') separators = ['に変換', 'は何', 'を'];
|
| 55 |
+
else if (lang === 'en') separators = ['convert', ' to ', ' in '];
|
| 56 |
+
else separators = [' sang ', ' ra '];
|
| 57 |
+
|
| 58 |
+
for (var i = 0; i < separators.length; i++) {
|
| 59 |
+
var sep = separators[i];
|
| 60 |
+
var idx = text.toLowerCase().indexOf(sep.toLowerCase());
|
| 61 |
+
if (idx === -1) continue;
|
| 62 |
+
|
| 63 |
+
var leftPart = text.substring(0, idx).trim();
|
| 64 |
+
var rightPart = text.substring(idx + sep.length).trim();
|
| 65 |
+
if (leftPart.length === 0 || rightPart.length === 0) continue;
|
| 66 |
+
|
| 67 |
+
var leftMatch = leftPart.match(/(-?\d+(?:\.\d+)?)\s*(.+)/);
|
| 68 |
+
if (!leftMatch) continue;
|
| 69 |
+
|
| 70 |
+
var value = parseFloat(leftMatch[1]);
|
| 71 |
+
var fromUnit = leftMatch[2].trim().toLowerCase();
|
| 72 |
+
var toUnit = rightPart.trim().toLowerCase();
|
| 73 |
+
if (isNaN(value)) continue;
|
| 74 |
+
|
| 75 |
+
return { value: value, from: fromUnit, to: toUnit };
|
| 76 |
+
}
|
| 77 |
+
return null;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function getSupportedUnits() {
|
| 81 |
+
var units = [];
|
| 82 |
+
var categories = Object.keys(CONVERSION_FACTORS);
|
| 83 |
+
for (var i = 0; i < categories.length; i++) {
|
| 84 |
+
units = units.concat(Object.keys(CONVERSION_FACTORS[categories[i]]));
|
| 85 |
+
}
|
| 86 |
+
units = units.concat(['celsius', 'fahrenheit', 'kelvin', 'c', 'f', 'k']);
|
| 87 |
+
return units.join(', ');
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function unitConversionAdapter(rs, args) {
|
| 91 |
+
_adapterPath.push('unit_conversion');
|
| 92 |
+
var input = (args || []).join(' ').trim();
|
| 93 |
+
var lang = currentLang || 'vi';
|
| 94 |
+
|
| 95 |
+
if (input.length === 0) {
|
| 96 |
+
if (lang === 'en') return 'Please provide a conversion request (e.g., "5 km to m").';
|
| 97 |
+
if (lang === 'ja') return '変換リクエストを入力してください(例: 「5 km を m」)。';
|
| 98 |
+
return 'Vui lòng nhập yêu cầu chuyển đổi (ví dụ: "5 km sang m").';
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
var parsed = parseConversionRequest(input, lang);
|
| 102 |
+
if (!parsed) {
|
| 103 |
+
if (lang === 'en') return 'Invalid syntax. Example: "5 km to m", "100 fahrenheit to celsius".';
|
| 104 |
+
if (lang === 'ja') return '構文が無効です。例: 「5 km を m」、「100 fahrenheit を celsius」。';
|
| 105 |
+
return 'Cú pháp không hợp lệ. Ví dụ: "5 km sang m", "100 fahrenheit sang celsius".';
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
var allUnits = {};
|
| 109 |
+
var categories = Object.keys(CONVERSION_FACTORS);
|
| 110 |
+
for (var i = 0; i < categories.length; i++) {
|
| 111 |
+
var catUnits = Object.keys(CONVERSION_FACTORS[categories[i]]);
|
| 112 |
+
for (var j = 0; j < catUnits.length; j++) allUnits[catUnits[j]] = true;
|
| 113 |
+
}
|
| 114 |
+
var tempKeys = Object.keys(TEMPERATURE_UNITS);
|
| 115 |
+
for (var t = 0; t < tempKeys.length; t++) allUnits[tempKeys[t]] = true;
|
| 116 |
+
|
| 117 |
+
if (!allUnits[parsed.from.toLowerCase()] || !allUnits[parsed.to.toLowerCase()]) {
|
| 118 |
+
var supported = getSupportedUnits();
|
| 119 |
+
if (lang === 'en') return 'Unsupported unit. Supported units: ' + supported;
|
| 120 |
+
if (lang === 'ja') return 'サポートされていない単位です。対応単位: ' + supported;
|
| 121 |
+
return 'Đơn vị không được hỗ trợ. Các đơn vị hỗ trợ: ' + supported;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
var result = convertUnit(parsed.value, parsed.from.toLowerCase(), parsed.to.toLowerCase());
|
| 125 |
+
if (result === null) {
|
| 126 |
+
var supported2 = getSupportedUnits();
|
| 127 |
+
if (lang === 'en') return 'Cannot convert between incompatible units. Supported units: ' + supported2;
|
| 128 |
+
if (lang === 'ja') return '互換性のない単位間の変換はできません。対応単位: ' + supported2;
|
| 129 |
+
return 'Không thể chuyển đổi giữa các đơn vị không tương thích. Các đơn vị hỗ trợ: ' + supported2;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
var resultStr = Number.isInteger(result) ? String(result) : result.toFixed(4).replace(/\.?0+$/, '');
|
| 133 |
+
return parsed.value + ' ' + parsed.from + ' = ' + resultStr + ' ' + parsed.to;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Node/test: export to globalThis
|
| 137 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 138 |
+
globalThis.CONVERSION_FACTORS = CONVERSION_FACTORS;
|
| 139 |
+
globalThis.TEMPERATURE_UNITS = TEMPERATURE_UNITS;
|
| 140 |
+
globalThis.convertTemperature = convertTemperature;
|
| 141 |
+
globalThis.convertUnit = convertUnit;
|
| 142 |
+
globalThis.parseConversionRequest = parseConversionRequest;
|
| 143 |
+
globalThis.getSupportedUnits = getSupportedUnits;
|
| 144 |
+
globalThis.unitConversionAdapter = unitConversionAdapter;
|
| 145 |
+
}
|
adapters/web-search.js
ADDED
|
@@ -0,0 +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 |
+
}
|
app.js
ADDED
|
@@ -0,0 +1,1108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// Hikari Chatbot — app.js
|
| 3 |
+
// Khung cơ bản và cấu hình (Task 1.3)
|
| 4 |
+
// ============================================================
|
| 5 |
+
|
| 6 |
+
// === Trạng thái toàn cục ===
|
| 7 |
+
let bot = null; // Instance RiveScript hiện tại
|
| 8 |
+
let currentLang = 'vi'; // Ngôn ngữ hiện tại
|
| 9 |
+
const USERNAME = 'local-user'; // Username cố định cho RiveScript
|
| 10 |
+
var _adapterPath = []; // Tracking adapter chain cho mỗi lượt phản hồi
|
| 11 |
+
|
| 12 |
+
// === Hằng số cấu hình ===
|
| 13 |
+
const FALLBACK_API_URL = 'https://your-api-server.com/chat'; // URL có thể cấu hình
|
| 14 |
+
const FALLBACK_API_TIMEOUT = 5000; // 5 giây
|
| 15 |
+
|
| 16 |
+
// ============================================================
|
| 17 |
+
// Hàm tiện ích
|
| 18 |
+
// ============================================================
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Bảng ánh xạ ký tự tiếng Việt có dấu → không dấu.
|
| 22 |
+
* Dùng để normalize input trước khi gửi cho RiveScript (triggers viết không dấu).
|
| 23 |
+
*/
|
| 24 |
+
var VIETNAMESE_DIACRITICS_MAP = {
|
| 25 |
+
'à': 'a', 'á': 'a', 'ả': 'a', 'ã': 'a', 'ạ': 'a',
|
| 26 |
+
'ă': 'a', 'ằ': 'a', 'ắ': 'a', 'ẳ': 'a', 'ẵ': 'a', 'ặ': 'a',
|
| 27 |
+
'â': 'a', 'ầ': 'a', 'ấ': 'a', 'ẩ': 'a', 'ẫ': 'a', 'ậ': 'a',
|
| 28 |
+
'đ': 'd',
|
| 29 |
+
'è': 'e', 'é': 'e', 'ẻ': 'e', 'ẽ': 'e', 'ẹ': 'e',
|
| 30 |
+
'ê': 'e', 'ề': 'e', 'ế': 'e', 'ể': 'e', 'ễ': 'e', 'ệ': 'e',
|
| 31 |
+
'ì': 'i', 'í': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ị': 'i',
|
| 32 |
+
'ò': 'o', 'ó': 'o', 'ỏ': 'o', 'õ': 'o', 'ọ': 'o',
|
| 33 |
+
'ô': 'o', 'ồ': 'o', 'ố': 'o', 'ổ': 'o', 'ỗ': 'o', 'ộ': 'o',
|
| 34 |
+
'ơ': 'o', 'ờ': 'o', 'ớ': 'o', 'ở': 'o', 'ỡ': 'o', 'ợ': 'o',
|
| 35 |
+
'ù': 'u', 'ú': 'u', 'ủ': 'u', 'ũ': 'u', 'ụ': 'u',
|
| 36 |
+
'ư': 'u', 'ừ': 'u', 'ứ': 'u', 'ử': 'u', 'ữ': 'u', 'ự': 'u',
|
| 37 |
+
'ỳ': 'y', 'ý': 'y', 'ỷ': 'y', 'ỹ': 'y', 'ỵ': 'y'
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Loại bỏ dấu tiếng Việt khỏi chuỗi.
|
| 42 |
+
* "Xin chào bạn" → "Xin chao ban"
|
| 43 |
+
* @param {string} str - Chuỗi đầu vào
|
| 44 |
+
* @returns {string} Chuỗi đã bỏ dấu
|
| 45 |
+
*/
|
| 46 |
+
function removeVietnameseDiacritics(str) {
|
| 47 |
+
if (typeof str !== 'string') return '';
|
| 48 |
+
var result = '';
|
| 49 |
+
for (var i = 0; i < str.length; i++) {
|
| 50 |
+
var ch = str[i];
|
| 51 |
+
var lower = ch.toLowerCase();
|
| 52 |
+
if (VIETNAMESE_DIACRITICS_MAP[lower] !== undefined) {
|
| 53 |
+
// Giữ nguyên case (hoa/thường) của ký tự gốc
|
| 54 |
+
var mapped = VIETNAMESE_DIACRITICS_MAP[lower];
|
| 55 |
+
result += (ch === lower) ? mapped : mapped.toUpperCase();
|
| 56 |
+
} else {
|
| 57 |
+
result += ch;
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
return result;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Danh sách modal particles / filler words theo ngôn ngữ.
|
| 65 |
+
* Các từ này thường xuất hiện cuối câu và không mang nghĩa chính,
|
| 66 |
+
* gây nhiễu khi match trigger RiveScript.
|
| 67 |
+
*/
|
| 68 |
+
var MODAL_PARTICLES = {
|
| 69 |
+
vi: ['roi', 'vay', 'nhi', 'nhe', 'a', 'nha', 'di', 'the', 'ha', 'hen', 'ne', 'luon', 'chua', 'khong', 'duoc', 'day', 'do', 'ta', 'chu'],
|
| 70 |
+
en: ['please', 'pls', 'right', 'huh', 'eh', 'ok', 'okay', 'well', 'so', 'then', 'anyway'],
|
| 71 |
+
ja: ['ね', 'よ', 'な', 'か', 'さ', 'ぞ', 'わ', 'の', 'けど', 'でしょ']
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* Normalize input cho RiveScript:
|
| 76 |
+
* 1. Lowercase
|
| 77 |
+
* 2. Bỏ dấu tiếng Việt (chỉ khi ngôn ngữ = vi)
|
| 78 |
+
* 3. Bỏ dấu câu (punctuation)
|
| 79 |
+
* 4. Bỏ modal particles / filler words cuối câu
|
| 80 |
+
* 5. Trim khoảng trắng thừa
|
| 81 |
+
*
|
| 82 |
+
* @param {string} text - Input người dùng
|
| 83 |
+
* @returns {string} Input đã normalize
|
| 84 |
+
*/
|
| 85 |
+
function normalizeInput(text) {
|
| 86 |
+
if (typeof text !== 'string') return '';
|
| 87 |
+
var result = text.toLowerCase();
|
| 88 |
+
|
| 89 |
+
// Bỏ dấu tiếng Việt
|
| 90 |
+
if (currentLang === 'vi') {
|
| 91 |
+
result = removeVietnameseDiacritics(result);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Bỏ dấu câu: ? ! . , ; : " ' ` ~ ( ) [ ] { } \ | @ # $ % ^ &
|
| 95 |
+
// Giữ lại + - * / cho biểu thức toán học
|
| 96 |
+
result = result.replace(/[?!.,;:"""''`~()[\]{}\\|@#$%^&]/g, ' ');
|
| 97 |
+
|
| 98 |
+
// Chuẩn hóa khoảng trắng
|
| 99 |
+
result = result.replace(/\s+/g, ' ').trim();
|
| 100 |
+
|
| 101 |
+
// Bỏ modal particles ở cuối câu (lặp để xử lý nhiều particle liên tiếp)
|
| 102 |
+
var particles = MODAL_PARTICLES[currentLang] || [];
|
| 103 |
+
if (particles.length > 0) {
|
| 104 |
+
var changed = true;
|
| 105 |
+
while (changed) {
|
| 106 |
+
changed = false;
|
| 107 |
+
for (var i = 0; i < particles.length; i++) {
|
| 108 |
+
var p = particles[i];
|
| 109 |
+
// Chỉ bỏ nếu particle là từ cuối cùng và câu còn ít nhất 1 từ khác
|
| 110 |
+
var suffix = ' ' + p;
|
| 111 |
+
if (result.length > suffix.length && result.slice(-suffix.length) === suffix) {
|
| 112 |
+
result = result.slice(0, -suffix.length).trim();
|
| 113 |
+
changed = true;
|
| 114 |
+
break;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return result;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* Kiểm tra tin nhắn có hợp lệ (không trống, không chỉ khoảng trắng).
|
| 125 |
+
* @param {string} text - Nội dung tin nhắn
|
| 126 |
+
* @returns {boolean} true nếu hợp lệ
|
| 127 |
+
*/
|
| 128 |
+
function validateMessage(text) {
|
| 129 |
+
return typeof text === 'string' && text.trim().length > 0;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* Chuyển URLs trong text thành thẻ <a> clickable.
|
| 134 |
+
* Escape HTML trước, rồi replace URL patterns thành links.
|
| 135 |
+
* @param {string} text
|
| 136 |
+
* @returns {string} HTML string
|
| 137 |
+
*/
|
| 138 |
+
function linkifyText(text) {
|
| 139 |
+
// Escape HTML entities trước
|
| 140 |
+
var escaped = String(text)
|
| 141 |
+
.replace(/&/g, '&')
|
| 142 |
+
.replace(/</g, '<')
|
| 143 |
+
.replace(/>/g, '>')
|
| 144 |
+
.replace(/"/g, '"');
|
| 145 |
+
// Replace newlines with <br>
|
| 146 |
+
escaped = escaped.replace(/\n/g, '<br>');
|
| 147 |
+
// Replace URLs with <a> tags
|
| 148 |
+
return escaped.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>');
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/**
|
| 152 |
+
* Cuộn message display xuống cuối.
|
| 153 |
+
*/
|
| 154 |
+
function scrollToBottom() {
|
| 155 |
+
const display = document.getElementById('message-display');
|
| 156 |
+
if (display) {
|
| 157 |
+
display.scrollTop = display.scrollHeight;
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Thêm tin nhắn vào DOM với class phân biệt user/bot, kèm confidence, adapter path và thời gian xử lý nếu là bot.
|
| 163 |
+
* @param {string} text - Nội dung tin nhắn
|
| 164 |
+
* @param {"user"|"bot"} sender - Người gửi
|
| 165 |
+
* @param {number} [confidence] - Tỉ lệ khớp (chỉ dùng cho bot)
|
| 166 |
+
* @param {string[]} [adapterPath] - Danh sách adapter đã xử lý (chỉ dùng cho bot)
|
| 167 |
+
* @param {number} [responseTime] - Thời gian xử lý (ms, chỉ dùng cho bot)
|
| 168 |
+
*/
|
| 169 |
+
function appendMessage(text, sender, confidence, adapterPath, responseTime) {
|
| 170 |
+
const display = document.getElementById('message-display');
|
| 171 |
+
if (!display) return;
|
| 172 |
+
|
| 173 |
+
const messageDiv = document.createElement('div');
|
| 174 |
+
messageDiv.className = 'message ' + sender;
|
| 175 |
+
|
| 176 |
+
const textSpan = document.createElement('span');
|
| 177 |
+
textSpan.className = 'message-text';
|
| 178 |
+
// Render URLs as clickable links for bot messages
|
| 179 |
+
if (sender === 'bot' && text.indexOf('http') !== -1) {
|
| 180 |
+
textSpan.innerHTML = linkifyText(text);
|
| 181 |
+
} else {
|
| 182 |
+
textSpan.textContent = text;
|
| 183 |
+
}
|
| 184 |
+
messageDiv.appendChild(textSpan);
|
| 185 |
+
|
| 186 |
+
if (sender === 'bot' && typeof confidence === 'number') {
|
| 187 |
+
const confSpan = document.createElement('span');
|
| 188 |
+
confSpan.className = 'confidence ' + getConfidenceClass(confidence);
|
| 189 |
+
confSpan.textContent = 'Confidence: ' + confidence + '%';
|
| 190 |
+
messageDiv.appendChild(confSpan);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
if (sender === 'bot' && adapterPath && adapterPath.length > 0) {
|
| 194 |
+
const pathSpan = document.createElement('span');
|
| 195 |
+
pathSpan.className = 'adapter-path';
|
| 196 |
+
var breadcrumb = adapterPath.map(function (key) {
|
| 197 |
+
return getAdapterDisplayName(key);
|
| 198 |
+
}).join(' › ');
|
| 199 |
+
pathSpan.textContent = breadcrumb;
|
| 200 |
+
messageDiv.appendChild(pathSpan);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
if (sender === 'bot' && typeof responseTime === 'number') {
|
| 204 |
+
const timeSpan = document.createElement('span');
|
| 205 |
+
timeSpan.className = 'response-time';
|
| 206 |
+
timeSpan.textContent = '⏱ ' + responseTime + 'ms';
|
| 207 |
+
messageDiv.appendChild(timeSpan);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
display.appendChild(messageDiv);
|
| 211 |
+
scrollToBottom();
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
/**
|
| 215 |
+
* Hiển thị thông báo lỗi trong chat.
|
| 216 |
+
* @param {string} message - Nội dung lỗi
|
| 217 |
+
*/
|
| 218 |
+
function showError(message) {
|
| 219 |
+
appendMessage(message, 'bot');
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/**
|
| 223 |
+
* Tính tỉ lệ khớp (confidence) dựa trên trigger đã match.
|
| 224 |
+
* - Trigger chính xác (không chứa wildcard) → 100
|
| 225 |
+
* - Trigger mặc định `*` → 0
|
| 226 |
+
* - Trigger chứa wildcard một phần → giá trị trung gian
|
| 227 |
+
* @param {string} matchedTrigger - Trigger đã khớp từ lastMatch()
|
| 228 |
+
* @returns {number} Confidence 0–100
|
| 229 |
+
*/
|
| 230 |
+
function calculateConfidence(matchedTrigger) {
|
| 231 |
+
if (typeof matchedTrigger !== 'string' || matchedTrigger.trim() === '') {
|
| 232 |
+
return 0;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
const trigger = matchedTrigger.trim();
|
| 236 |
+
|
| 237 |
+
// Trigger mặc định wildcard `*` → confidence = 0
|
| 238 |
+
if (trigger === '*') {
|
| 239 |
+
return 0;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Kiểm tra trigger có chứa wildcard không
|
| 243 |
+
const wildcardPatterns = ['*', '#', '_'];
|
| 244 |
+
const hasWildcard = wildcardPatterns.some(function (w) {
|
| 245 |
+
return trigger.includes(w);
|
| 246 |
+
});
|
| 247 |
+
|
| 248 |
+
if (!hasWildcard) {
|
| 249 |
+
// Trigger chính xác (không wildcard) → confidence = 100
|
| 250 |
+
return 100;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// Trigger chứa wildcard một phần → tính giá trị trung gian
|
| 254 |
+
// Dựa trên tỉ lệ phần không phải wildcard so với tổng chiều dài
|
| 255 |
+
const parts = trigger.split(/[\s]+/);
|
| 256 |
+
var nonWildcardParts = 0;
|
| 257 |
+
for (var i = 0; i < parts.length; i++) {
|
| 258 |
+
if (parts[i] !== '*' && parts[i] !== '#' && parts[i] !== '_') {
|
| 259 |
+
nonWildcardParts++;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
var ratio = nonWildcardParts / parts.length;
|
| 263 |
+
// Ánh xạ ratio vào khoảng [1, 99] để tránh trùng với 0 và 100
|
| 264 |
+
return Math.max(1, Math.min(99, Math.round(ratio * 100)));
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
/**
|
| 268 |
+
* Trả về CSS class cho màu sắc confidence.
|
| 269 |
+
* @param {number} confidence - Tỉ lệ khớp (0–100)
|
| 270 |
+
* @returns {string} "confidence-high" nếu ≥50, "confidence-low" nếu <50
|
| 271 |
+
*/
|
| 272 |
+
function getConfidenceClass(confidence) {
|
| 273 |
+
return confidence >= 50 ? 'confidence-high' : 'confidence-low';
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// ============================================================
|
| 277 |
+
// Brain Data — Dữ liệu hội thoại RiveScript cho 3 ngôn ngữ
|
| 278 |
+
// Được load từ file .rive riêng biệt trong thư mục brain/
|
| 279 |
+
// Xem: brain/vi.rive, brain/en.rive, brain/ja.rive
|
| 280 |
+
// Trong trình duyệt: BRAIN_DATA được load bởi brain.js (qua fetch)
|
| 281 |
+
// Trong Node/test: BRAIN_DATA được load bởi fs.readFileSync
|
| 282 |
+
// ============================================================
|
| 283 |
+
|
| 284 |
+
// Node/test: khai báo các biến dữ liệu ngoài (browser đã có từ brain.js & data-loader.js)
|
| 285 |
+
// Sử dụng IIFE để tránh var hoisting ghi đè biến global trong browser
|
| 286 |
+
(function () {
|
| 287 |
+
if (typeof module === 'undefined' || !module.exports) return;
|
| 288 |
+
var _fs = require('fs');
|
| 289 |
+
var _path = require('path');
|
| 290 |
+
globalThis.BRAIN_DATA = {
|
| 291 |
+
vi: _fs.readFileSync(_path.join(__dirname, 'brain', 'vi.rive'), 'utf8'),
|
| 292 |
+
en: _fs.readFileSync(_path.join(__dirname, 'brain', 'en.rive'), 'utf8'),
|
| 293 |
+
ja: _fs.readFileSync(_path.join(__dirname, 'brain', 'ja.rive'), 'utf8')
|
| 294 |
+
};
|
| 295 |
+
globalThis.SPECIFIC_RESPONSES = JSON.parse(_fs.readFileSync(_path.join(__dirname, 'data', 'specific-responses.json'), 'utf8'));
|
| 296 |
+
globalThis.QA_DATASET = JSON.parse(_fs.readFileSync(_path.join(__dirname, 'data', 'qa-dataset.json'), 'utf8'));
|
| 297 |
+
globalThis.ADAPTER_REGISTRY = JSON.parse(_fs.readFileSync(_path.join(__dirname, 'data', 'adapter-registry.json'), 'utf8'));
|
| 298 |
+
globalThis.HELP_CONTENT = JSON.parse(_fs.readFileSync(_path.join(__dirname, 'data', 'help-content.json'), 'utf8'));
|
| 299 |
+
})();
|
| 300 |
+
|
| 301 |
+
// ============================================================
|
| 302 |
+
// Lời chào mặc định cho mỗi ngôn ngữ (trigger gửi đến bot)
|
| 303 |
+
// ============================================================
|
| 304 |
+
const GREETING_TRIGGERS = {
|
| 305 |
+
vi: 'xin chao',
|
| 306 |
+
en: 'hello',
|
| 307 |
+
ja: 'こんにちは'
|
| 308 |
+
};
|
| 309 |
+
|
| 310 |
+
// ============================================================
|
| 311 |
+
// Khởi tạo RiveScript Engine
|
| 312 |
+
// ============================================================
|
| 313 |
+
|
| 314 |
+
/**
|
| 315 |
+
* Khởi tạo RiveScript engine với brain data của ngôn ngữ chỉ định.
|
| 316 |
+
* Tạo instance mới, stream brain data, sort replies, đăng ký adapter.
|
| 317 |
+
* @param {string} lang - Mã ngôn ngữ ("vi", "en", "ja")
|
| 318 |
+
* @returns {Promise<void>}
|
| 319 |
+
*/
|
| 320 |
+
async function initBot(lang) {
|
| 321 |
+
// Kiểm tra CDN RiveScript đã tải chưa
|
| 322 |
+
if (typeof RiveScript === 'undefined') {
|
| 323 |
+
showError('Chatbot hiện không khả dụng. Vui lòng kiểm tra kết nối mạng và tải lại trang.');
|
| 324 |
+
return;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
try {
|
| 328 |
+
bot = new RiveScript({ utf8: true });
|
| 329 |
+
bot.stream(BRAIN_DATA[lang]);
|
| 330 |
+
bot.sortReplies();
|
| 331 |
+
|
| 332 |
+
// Đăng ký adapter nếu hàm registerAdapters đã được định nghĩa (sẽ thêm ở task sau)
|
| 333 |
+
if (typeof registerAdapters === 'function') {
|
| 334 |
+
registerAdapters(bot, lang);
|
| 335 |
+
}
|
| 336 |
+
} catch (err) {
|
| 337 |
+
console.error('Lỗi khởi tạo RiveScript:', err);
|
| 338 |
+
showError('Không thể khởi tạo chatbot. Vui lòng tải lại trang.');
|
| 339 |
+
bot = null;
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
/**
|
| 344 |
+
* Đổi ngôn ngữ: tạo lại bot, xóa chat, hiển thị lời chào mới,
|
| 345 |
+
* cập nhật rules list và macros list.
|
| 346 |
+
* @param {string} lang - Mã ngôn ngữ ("vi", "en", "ja")
|
| 347 |
+
* @returns {Promise<void>}
|
| 348 |
+
*/
|
| 349 |
+
async function changeLanguage(lang) {
|
| 350 |
+
currentLang = lang;
|
| 351 |
+
await initBot(lang);
|
| 352 |
+
|
| 353 |
+
// Xóa lịch sử hội thoại
|
| 354 |
+
var display = document.getElementById('message-display');
|
| 355 |
+
if (display) {
|
| 356 |
+
display.innerHTML = '';
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Hiển thị lời chào mới bằng ngôn ngữ được chọn
|
| 360 |
+
if (bot) {
|
| 361 |
+
try {
|
| 362 |
+
var greeting = await bot.reply(USERNAME, GREETING_TRIGGERS[lang] || 'hello');
|
| 363 |
+
appendMessage(greeting, 'bot');
|
| 364 |
+
} catch (err) {
|
| 365 |
+
console.error('Lỗi lấy lời chào:', err);
|
| 366 |
+
showError('Không thể hiển thị lời chào.');
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// Cập nhật rules list nếu hàm đã được định nghĩa (sẽ thêm ở task sau)
|
| 371 |
+
if (typeof updateRulesList === 'function') {
|
| 372 |
+
updateRulesList(lang);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Cập nhật macros list nếu hàm đã được định nghĩa (sẽ thêm ở task sau)
|
| 376 |
+
if (typeof updateMacrosList === 'function') {
|
| 377 |
+
updateMacrosList(lang);
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// ============================================================
|
| 382 |
+
// Gửi tin nhắn và xử lý phản hồi
|
| 383 |
+
// ============================================================
|
| 384 |
+
|
| 385 |
+
/**
|
| 386 |
+
* Hiển thị chỉ báo đang tải (loading indicator) trong message display.
|
| 387 |
+
* @returns {HTMLElement} Phần tử loading indicator đã thêm vào DOM
|
| 388 |
+
*/
|
| 389 |
+
function showLoadingIndicator() {
|
| 390 |
+
var display = document.getElementById('message-display');
|
| 391 |
+
if (!display) return null;
|
| 392 |
+
|
| 393 |
+
var loadingDiv = document.createElement('div');
|
| 394 |
+
loadingDiv.className = 'message bot loading-indicator';
|
| 395 |
+
loadingDiv.textContent = '...';
|
| 396 |
+
display.appendChild(loadingDiv);
|
| 397 |
+
scrollToBottom();
|
| 398 |
+
return loadingDiv;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
/**
|
| 402 |
+
* Ẩn/xóa chỉ báo đang tải.
|
| 403 |
+
* @param {HTMLElement} element - Phần tử loading indicator cần xóa
|
| 404 |
+
*/
|
| 405 |
+
function hideLoadingIndicator(element) {
|
| 406 |
+
if (element && element.parentNode) {
|
| 407 |
+
element.parentNode.removeChild(element);
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
/**
|
| 412 |
+
* Gửi HTTP POST đến Fallback API với timeout 5s.
|
| 413 |
+
* Sử dụng AbortController để hủy request khi vượt quá thời gian chờ.
|
| 414 |
+
* @param {string} userMessage - Tin nhắn người dùng
|
| 415 |
+
* @returns {Promise<string|null>} Phản hồi từ API hoặc null nếu lỗi/timeout
|
| 416 |
+
*/
|
| 417 |
+
async function callFallbackAPI(userMessage) {
|
| 418 |
+
var controller = new AbortController();
|
| 419 |
+
var timeoutId = setTimeout(function () {
|
| 420 |
+
controller.abort();
|
| 421 |
+
}, FALLBACK_API_TIMEOUT);
|
| 422 |
+
|
| 423 |
+
try {
|
| 424 |
+
var response = await fetch(FALLBACK_API_URL, {
|
| 425 |
+
method: 'POST',
|
| 426 |
+
headers: { 'Content-Type': 'application/json' },
|
| 427 |
+
body: JSON.stringify({ message: userMessage }),
|
| 428 |
+
signal: controller.signal
|
| 429 |
+
});
|
| 430 |
+
|
| 431 |
+
var data = await response.json();
|
| 432 |
+
return data.answer || data.reply || data.response || null;
|
| 433 |
+
} catch (err) {
|
| 434 |
+
// Timeout (AbortError) hoặc lỗi mạng → trả về null
|
| 435 |
+
return null;
|
| 436 |
+
} finally {
|
| 437 |
+
clearTimeout(timeoutId);
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
/**
|
| 442 |
+
* Detect nếu input là web search command.
|
| 443 |
+
* Trả về query string nếu match, null nếu không.
|
| 444 |
+
* @param {string} text - Input người dùng (raw)
|
| 445 |
+
* @returns {string|null} Query string hoặc null
|
| 446 |
+
*/
|
| 447 |
+
function extractWebSearchQuery(text) {
|
| 448 |
+
var lower = text.toLowerCase().trim();
|
| 449 |
+
// Normalize tiếng Việt để match "tra cứu" → "tra cuu"
|
| 450 |
+
var norm = (typeof normalizeInput === 'function') ? normalizeInput(text).toLowerCase().trim() : lower;
|
| 451 |
+
|
| 452 |
+
var prefixes = [
|
| 453 |
+
'google ', 'tra cuu ', 'tra cứu ',
|
| 454 |
+
'search ', 'web search ',
|
| 455 |
+
'ウェブ検索 ', 'グーグル ',
|
| 456 |
+
'tìm trên web ', 'tìm trên mạng ', 'tim tren web ', 'tim tren mang '
|
| 457 |
+
];
|
| 458 |
+
|
| 459 |
+
for (var i = 0; i < prefixes.length; i++) {
|
| 460 |
+
if (lower.indexOf(prefixes[i]) === 0) {
|
| 461 |
+
return text.substring(prefixes[i].length).trim();
|
| 462 |
+
}
|
| 463 |
+
if (norm.indexOf(prefixes[i]) === 0) {
|
| 464 |
+
return text.substring(prefixes[i].length).trim();
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
return null;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
/**
|
| 471 |
+
* Lấy phản hồi từ bot cho một input, trả về reply + confidence + adapterPath.
|
| 472 |
+
* @param {string} inputText - Input gửi cho bot
|
| 473 |
+
* @returns {Promise<{reply: string, confidence: number, adapterPath: string[]}>}
|
| 474 |
+
*/
|
| 475 |
+
async function getBotReply(inputText) {
|
| 476 |
+
_adapterPath = [];
|
| 477 |
+
var reply = await bot.reply(USERNAME, inputText);
|
| 478 |
+
var matchedTrigger = await bot.lastMatch(USERNAME);
|
| 479 |
+
var confidence = calculateConfidence(matchedTrigger);
|
| 480 |
+
var adapterPath = _adapterPath.length > 0 ? _adapterPath.slice() : ['rivescript'];
|
| 481 |
+
return { reply: reply, confidence: confidence, adapterPath: adapterPath };
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
/**
|
| 485 |
+
* Lấy tin nhắn từ input, gửi đến bot, tính confidence, xử lý fallback, hiển thị kết quả.
|
| 486 |
+
*
|
| 487 |
+
* Chiến lược dual-reply (chỉ áp dụng cho tiếng Việt):
|
| 488 |
+
* - Gửi cả input gốc (có dấu) VÀ input đã bỏ dấu cho bot
|
| 489 |
+
* - So sánh confidence của 2 kết quả, chọn kết quả có confidence cao hơn
|
| 490 |
+
* - Nếu bằng nhau, ưu tiên kết quả từ input gốc (có dấu)
|
| 491 |
+
*
|
| 492 |
+
* @returns {Promise<void>}
|
| 493 |
+
*/
|
| 494 |
+
async function sendMessage() {
|
| 495 |
+
var input = document.getElementById('message-input');
|
| 496 |
+
if (!input) return;
|
| 497 |
+
|
| 498 |
+
var text = input.value;
|
| 499 |
+
|
| 500 |
+
// Kiểm tra tin nhắn hợp lệ
|
| 501 |
+
if (!validateMessage(text)) {
|
| 502 |
+
return;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
// Hiển thị tin nhắn người dùng
|
| 506 |
+
appendMessage(text, 'user');
|
| 507 |
+
|
| 508 |
+
// Xóa input
|
| 509 |
+
input.value = '';
|
| 510 |
+
|
| 511 |
+
// Kiểm tra bot đã khởi tạo chưa
|
| 512 |
+
if (!bot) {
|
| 513 |
+
showError('Chatbot chưa sẵn sàng. Vui lòng tải lại trang.');
|
| 514 |
+
return;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
try {
|
| 518 |
+
var startTime = Date.now();
|
| 519 |
+
|
| 520 |
+
// Web Search: detect "google ...", "tra cuu ...", "search ...", "web search ..." trước khi gửi cho RiveScript
|
| 521 |
+
// Xử l�� trực tiếp vì web search là async và RiveScript subroutine không hỗ trợ Promise
|
| 522 |
+
var webSearchQuery = extractWebSearchQuery(text);
|
| 523 |
+
if (webSearchQuery && typeof webSearchAdapter === 'function') {
|
| 524 |
+
var loadingEl = showLoadingIndicator();
|
| 525 |
+
try {
|
| 526 |
+
_adapterPath = [];
|
| 527 |
+
var searchResult = await webSearchAdapter(null, webSearchQuery.split(/\s+/));
|
| 528 |
+
hideLoadingIndicator(loadingEl);
|
| 529 |
+
var elapsed = Date.now() - startTime;
|
| 530 |
+
var searchPath = _adapterPath.length > 0 ? _adapterPath.slice() : ['web_search'];
|
| 531 |
+
appendMessage(searchResult, 'bot', null, searchPath, elapsed);
|
| 532 |
+
} catch (searchErr) {
|
| 533 |
+
hideLoadingIndicator(loadingEl);
|
| 534 |
+
showError('Tìm kiếm thất bại. Vui lòng thử lại.');
|
| 535 |
+
}
|
| 536 |
+
scrollToBottom();
|
| 537 |
+
return;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
var best;
|
| 541 |
+
var normalizedText = normalizeInput(text);
|
| 542 |
+
var isDifferent = normalizedText !== text;
|
| 543 |
+
|
| 544 |
+
if (currentLang === 'vi' && isDifferent) {
|
| 545 |
+
var rawResult = await getBotReply(text);
|
| 546 |
+
var normResult = await getBotReply(normalizedText);
|
| 547 |
+
best = (normResult.confidence > rawResult.confidence) ? normResult : rawResult;
|
| 548 |
+
} else {
|
| 549 |
+
best = await getBotReply(text);
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
var reply = best.reply;
|
| 553 |
+
var confidence = best.confidence;
|
| 554 |
+
var adapterPath = best.adapterPath;
|
| 555 |
+
|
| 556 |
+
if (confidence >= 50) {
|
| 557 |
+
var elapsed = Date.now() - startTime;
|
| 558 |
+
appendMessage(reply, 'bot', confidence, adapterPath, elapsed);
|
| 559 |
+
} else {
|
| 560 |
+
_adapterPath = [];
|
| 561 |
+
var localFallback = bestMatchAdapter(bot, text.toLowerCase().split(/\s+/));
|
| 562 |
+
var localPath = _adapterPath.slice();
|
| 563 |
+
|
| 564 |
+
if (currentLang === 'vi') {
|
| 565 |
+
_adapterPath = [];
|
| 566 |
+
var normFallback = bestMatchAdapter(bot, normalizeInput(text).toLowerCase().split(/\s+/));
|
| 567 |
+
var normPath = _adapterPath.slice();
|
| 568 |
+
if (isValidAdapterResult(normFallback) && !isValidAdapterResult(localFallback)) {
|
| 569 |
+
localFallback = normFallback;
|
| 570 |
+
localPath = normPath;
|
| 571 |
+
}
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
if (isValidAdapterResult(localFallback)) {
|
| 575 |
+
var elapsed2 = Date.now() - startTime;
|
| 576 |
+
appendMessage(localFallback, 'bot', confidence, localPath, elapsed2);
|
| 577 |
+
} else {
|
| 578 |
+
var loadingEl = showLoadingIndicator();
|
| 579 |
+
|
| 580 |
+
try {
|
| 581 |
+
var apiResult = await callFallbackAPI(text);
|
| 582 |
+
hideLoadingIndicator(loadingEl);
|
| 583 |
+
var elapsed3 = Date.now() - startTime;
|
| 584 |
+
|
| 585 |
+
if (apiResult) {
|
| 586 |
+
var apiPath = adapterPath.concat(['fallback_api']);
|
| 587 |
+
appendMessage(apiResult, 'bot', confidence, apiPath, elapsed3);
|
| 588 |
+
} else {
|
| 589 |
+
appendMessage(reply + '\n(Dịch vụ bổ sung không khả dụng)', 'bot', confidence, adapterPath, elapsed3);
|
| 590 |
+
}
|
| 591 |
+
} catch (apiErr) {
|
| 592 |
+
hideLoadingIndicator(loadingEl);
|
| 593 |
+
var elapsed4 = Date.now() - startTime;
|
| 594 |
+
appendMessage(reply + '\n(Dịch vụ bổ sung không khả dụng)', 'bot', confidence, adapterPath, elapsed4);
|
| 595 |
+
}
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
} catch (err) {
|
| 599 |
+
console.error('Lỗi xử lý tin nhắn:', err);
|
| 600 |
+
showError('Xin lỗi, mình gặp sự cố. Bạn thử gửi lại nhé!');
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
scrollToBottom();
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
// ============================================================
|
| 607 |
+
// Logic Adapters — Tách thành các file riêng trong thư mục adapters/
|
| 608 |
+
// Xem: adapters/text-similarity.js, adapters/specific-response.js,
|
| 609 |
+
// adapters/time-adapter.js, adapters/math-adapter.js,
|
| 610 |
+
// adapters/unit-conversion.js, adapters/best-match.js,
|
| 611 |
+
// adapters/logic-dispatcher.js, adapters/adapter-registry.js
|
| 612 |
+
// Trong trình duyệt: được load bởi <script> tags trong index.html
|
| 613 |
+
// Trong Node/test: được load bởi IIFE bên dưới
|
| 614 |
+
// ============================================================
|
| 615 |
+
|
| 616 |
+
// Node/test: expose shared variables to globalThis for adapter files
|
| 617 |
+
(function () {
|
| 618 |
+
if (typeof module === 'undefined' || !module.exports) return;
|
| 619 |
+
// Adapter files access these as free variables — in Node they need to be on globalThis
|
| 620 |
+
// Use Object.defineProperty for _adapterPath and currentLang to keep them in sync with app.js locals
|
| 621 |
+
Object.defineProperty(globalThis, '_adapterPath', {
|
| 622 |
+
get: function () { return _adapterPath; },
|
| 623 |
+
set: function (val) { _adapterPath = val; },
|
| 624 |
+
configurable: true
|
| 625 |
+
});
|
| 626 |
+
Object.defineProperty(globalThis, 'currentLang', {
|
| 627 |
+
get: function () { return currentLang; },
|
| 628 |
+
set: function (val) { currentLang = val; },
|
| 629 |
+
configurable: true
|
| 630 |
+
});
|
| 631 |
+
globalThis.removeVietnameseDiacritics = removeVietnameseDiacritics;
|
| 632 |
+
})();
|
| 633 |
+
|
| 634 |
+
// Node/test: load adapter files
|
| 635 |
+
(function () {
|
| 636 |
+
if (typeof module === 'undefined' || !module.exports) return;
|
| 637 |
+
var _path = require('path');
|
| 638 |
+
var _adDir = _path.join(__dirname, 'adapters');
|
| 639 |
+
var files = [
|
| 640 |
+
'text-similarity.js',
|
| 641 |
+
'specific-response.js',
|
| 642 |
+
'time-adapter.js',
|
| 643 |
+
'math-adapter.js',
|
| 644 |
+
'unit-conversion.js',
|
| 645 |
+
'best-match.js',
|
| 646 |
+
'logic-dispatcher.js',
|
| 647 |
+
'web-search.js',
|
| 648 |
+
'adapter-registry.js'
|
| 649 |
+
];
|
| 650 |
+
for (var i = 0; i < files.length; i++) {
|
| 651 |
+
require(_path.join(_adDir, files[i]));
|
| 652 |
+
}
|
| 653 |
+
// Load preprocessed data synchronously for Node/test
|
| 654 |
+
if (typeof loadPreprocessedData === 'function') {
|
| 655 |
+
loadPreprocessedData();
|
| 656 |
+
}
|
| 657 |
+
})();
|
| 658 |
+
|
| 659 |
+
// ============================================================
|
| 660 |
+
// Object Macro List Panel — Toggle và cập nhật danh sách macros
|
| 661 |
+
// ============================================================
|
| 662 |
+
|
| 663 |
+
/**
|
| 664 |
+
* Toggle hiển thị panel danh sách Object Macros.
|
| 665 |
+
* Thêm/xóa class 'hidden' trên phần tử #macros-panel.
|
| 666 |
+
*/
|
| 667 |
+
function toggleMacrosPanel() {
|
| 668 |
+
var panel = document.getElementById('macros-panel');
|
| 669 |
+
if (panel) {
|
| 670 |
+
panel.classList.toggle('hidden');
|
| 671 |
+
}
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
/**
|
| 675 |
+
* Cập nhật danh sách Object Macros cho ngôn ngữ chỉ định.
|
| 676 |
+
* Xóa nội dung cũ của #macros-list, duyệt qua ADAPTER_REGISTRY,
|
| 677 |
+
* tạo <li> cho mỗi adapter với tên, mô tả, và cú pháp <call>.
|
| 678 |
+
*
|
| 679 |
+
* @param {string} lang - Mã ngôn ngữ ("vi", "en", "ja")
|
| 680 |
+
*/
|
| 681 |
+
function updateMacrosList(lang) {
|
| 682 |
+
var list = document.getElementById('macros-list');
|
| 683 |
+
if (!list) return;
|
| 684 |
+
|
| 685 |
+
list.innerHTML = '';
|
| 686 |
+
|
| 687 |
+
var activeLabel = lang === 'en' ? 'Active' : lang === 'ja' ? '有効' : 'Hoạt động';
|
| 688 |
+
|
| 689 |
+
var keys = Object.keys(ADAPTER_REGISTRY);
|
| 690 |
+
for (var i = 0; i < keys.length; i++) {
|
| 691 |
+
var key = keys[i];
|
| 692 |
+
var adapter = ADAPTER_REGISTRY[key];
|
| 693 |
+
|
| 694 |
+
var li = document.createElement('li');
|
| 695 |
+
li.className = 'macro-item';
|
| 696 |
+
|
| 697 |
+
// Header row: name + badge
|
| 698 |
+
var header = document.createElement('div');
|
| 699 |
+
header.className = 'macro-item-header';
|
| 700 |
+
|
| 701 |
+
var strong = document.createElement('strong');
|
| 702 |
+
strong.textContent = adapter.name[lang] || adapter.name['vi'];
|
| 703 |
+
header.appendChild(strong);
|
| 704 |
+
|
| 705 |
+
if (adapter.active) {
|
| 706 |
+
var badge = document.createElement('span');
|
| 707 |
+
badge.className = 'macro-item-badge';
|
| 708 |
+
badge.textContent = activeLabel;
|
| 709 |
+
header.appendChild(badge);
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
li.appendChild(header);
|
| 713 |
+
|
| 714 |
+
var p = document.createElement('p');
|
| 715 |
+
p.textContent = adapter.description[lang] || adapter.description['vi'];
|
| 716 |
+
li.appendChild(p);
|
| 717 |
+
|
| 718 |
+
var span = document.createElement('span');
|
| 719 |
+
span.className = 'macro-call-syntax';
|
| 720 |
+
span.textContent = adapter.callSyntax;
|
| 721 |
+
li.appendChild(span);
|
| 722 |
+
|
| 723 |
+
list.appendChild(li);
|
| 724 |
+
}
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
// ============================================================
|
| 728 |
+
// Rules Panel — Hiển thị danh sách triggers dạng dễ đọc
|
| 729 |
+
// ============================================================
|
| 730 |
+
|
| 731 |
+
/**
|
| 732 |
+
* Toggle hiển thị panel danh sách rules khi click nút #rules-button.
|
| 733 |
+
* Thêm/xóa class 'hidden' trên #rules-panel.
|
| 734 |
+
*/
|
| 735 |
+
function toggleRulesPanel() {
|
| 736 |
+
var panel = document.getElementById('rules-panel');
|
| 737 |
+
if (panel) {
|
| 738 |
+
panel.classList.toggle('hidden');
|
| 739 |
+
}
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
/**
|
| 743 |
+
* Cập nhật danh sách triggers cho ngôn ngữ chỉ định.
|
| 744 |
+
* Lấy triggers từ bot.getUserTopicTriggers(), lọc bỏ wildcard mặc định '*',
|
| 745 |
+
* format dạng dễ đọc và hiển thị trong #rules-list.
|
| 746 |
+
* @param {string} lang - Mã ngôn ngữ ("vi", "en", "ja")
|
| 747 |
+
*/
|
| 748 |
+
function updateRulesList(lang) {
|
| 749 |
+
var list = document.getElementById('rules-list');
|
| 750 |
+
if (!list) return;
|
| 751 |
+
|
| 752 |
+
// Xóa nội dung cũ
|
| 753 |
+
list.innerHTML = '';
|
| 754 |
+
|
| 755 |
+
// Nếu bot chưa khởi tạo, hiển thị thông báo
|
| 756 |
+
if (!bot) {
|
| 757 |
+
var emptyLi = document.createElement('li');
|
| 758 |
+
emptyLi.textContent = lang === 'en'
|
| 759 |
+
? 'Bot is not initialized.'
|
| 760 |
+
: lang === 'ja'
|
| 761 |
+
? 'ボットが初期化されていません。'
|
| 762 |
+
: 'Bot chưa được khởi tạo.';
|
| 763 |
+
list.appendChild(emptyLi);
|
| 764 |
+
return;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
try {
|
| 768 |
+
// Lấy danh sách triggers từ internal data của RiveScript
|
| 769 |
+
var triggers = [];
|
| 770 |
+
var topicData = bot._topics && bot._topics.random;
|
| 771 |
+
if (topicData) {
|
| 772 |
+
var keys = Object.keys(topicData);
|
| 773 |
+
for (var k = 0; k < keys.length; k++) {
|
| 774 |
+
var entry = topicData[keys[k]];
|
| 775 |
+
if (entry && entry.trigger) {
|
| 776 |
+
triggers.push(entry.trigger);
|
| 777 |
+
}
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
if (triggers.length === 0) {
|
| 782 |
+
return;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// Dùng Set để loại bỏ trigger trùng lặp (wildcard variants cùng response)
|
| 786 |
+
var seen = {};
|
| 787 |
+
for (var i = 0; i < triggers.length; i++) {
|
| 788 |
+
var trigger = triggers[i];
|
| 789 |
+
|
| 790 |
+
// Lọc bỏ trigger wildcard mặc định '*' và trigger chỉ chứa wildcard
|
| 791 |
+
if (trigger.trim() === '*') continue;
|
| 792 |
+
// Lọc bỏ trigger dạng "* la ai" (chỉ wildcard + từ) — giữ trigger có nội dung rõ ràng
|
| 793 |
+
if (/^\*\s/.test(trigger.trim()) && trigger.trim().split(/\s+/).length <= 3) continue;
|
| 794 |
+
|
| 795 |
+
var formatted = formatTrigger(trigger);
|
| 796 |
+
if (formatted.length === 0 || seen[formatted]) continue;
|
| 797 |
+
seen[formatted] = true;
|
| 798 |
+
|
| 799 |
+
var li = document.createElement('li');
|
| 800 |
+
li.textContent = formatted;
|
| 801 |
+
list.appendChild(li);
|
| 802 |
+
}
|
| 803 |
+
} catch (err) {
|
| 804 |
+
console.error('Lỗi lấy danh sách triggers:', err);
|
| 805 |
+
}
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
/**
|
| 809 |
+
* Chuyển trigger RiveScript thành dạng dễ đọc cho người dùng.
|
| 810 |
+
* - Thay '*' bằng '...'
|
| 811 |
+
* - Xóa cú pháp RiveScript như <call>...</call>, <star>, <bot ...>
|
| 812 |
+
* - Xóa ký tự thừa và trim khoảng trắng
|
| 813 |
+
* @param {string} trigger - Chuỗi trigger RiveScript
|
| 814 |
+
* @returns {string} Chuỗi đã format dễ đọc
|
| 815 |
+
*/
|
| 816 |
+
function formatTrigger(trigger) {
|
| 817 |
+
if (typeof trigger !== 'string') return '';
|
| 818 |
+
|
| 819 |
+
var result = trigger;
|
| 820 |
+
|
| 821 |
+
// Xóa thẻ <call>...</call> và nội dung bên trong
|
| 822 |
+
result = result.replace(/<call>[^<]*<\/call>/g, '');
|
| 823 |
+
|
| 824 |
+
// Xóa các thẻ RiveScript khác: <star>, <bot ...>, <input>, <reply>, v.v.
|
| 825 |
+
result = result.replace(/<[^>]+>/g, '');
|
| 826 |
+
|
| 827 |
+
// Thay wildcard '*' bằng '...'
|
| 828 |
+
result = result.replace(/\*/g, '...');
|
| 829 |
+
|
| 830 |
+
// Thay '#' (number wildcard) bằng '[số]'
|
| 831 |
+
result = result.replace(/#/g, '[số]');
|
| 832 |
+
|
| 833 |
+
// Thay '_' (word wildcard) bằng '[từ]'
|
| 834 |
+
result = result.replace(/_/g, '[từ]');
|
| 835 |
+
|
| 836 |
+
// Xóa khoảng trắng thừa
|
| 837 |
+
result = result.replace(/\s+/g, ' ').trim();
|
| 838 |
+
|
| 839 |
+
return result;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
// ============================================================
|
| 843 |
+
// Help Dialog — Popup hướng dẫn sử dụng đa ngôn ngữ
|
| 844 |
+
// ============================================================
|
| 845 |
+
|
| 846 |
+
/**
|
| 847 |
+
* Nội dung hướng dẫn sử dụng — load từ data/help-content.json
|
| 848 |
+
* Browser: HELP_CONTENT được khai báo bởi data-loader.js
|
| 849 |
+
* Node/test: HELP_CONTENT được khai báo ở block đầu file
|
| 850 |
+
*/
|
| 851 |
+
|
| 852 |
+
/**
|
| 853 |
+
* Render nội dung help dialog theo ngôn ngữ hiện tại.
|
| 854 |
+
*/
|
| 855 |
+
function renderHelpContent() {
|
| 856 |
+
var body = document.getElementById('help-dialog-body');
|
| 857 |
+
var titleEl = document.getElementById('help-dialog-title');
|
| 858 |
+
if (!body) return;
|
| 859 |
+
|
| 860 |
+
var lang = currentLang || 'vi';
|
| 861 |
+
var content = HELP_CONTENT[lang] || HELP_CONTENT['vi'];
|
| 862 |
+
|
| 863 |
+
if (titleEl) {
|
| 864 |
+
titleEl.textContent = content.title;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
var html = '';
|
| 868 |
+
for (var i = 0; i < content.sections.length; i++) {
|
| 869 |
+
var section = content.sections[i];
|
| 870 |
+
html += '<div class="help-section">';
|
| 871 |
+
html += '<h3>' + section.icon + ' ' + section.heading + '</h3>';
|
| 872 |
+
html += '<ul>';
|
| 873 |
+
for (var j = 0; j < section.items.length; j++) {
|
| 874 |
+
html += '<li>' + section.items[j] + '</li>';
|
| 875 |
+
}
|
| 876 |
+
html += '</ul>';
|
| 877 |
+
html += '</div>';
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
body.innerHTML = html;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
/**
|
| 884 |
+
* Mở help dialog.
|
| 885 |
+
*/
|
| 886 |
+
function openHelpDialog() {
|
| 887 |
+
var overlay = document.getElementById('help-overlay');
|
| 888 |
+
if (!overlay) return;
|
| 889 |
+
renderHelpContent();
|
| 890 |
+
overlay.classList.remove('hidden');
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
/**
|
| 894 |
+
* Đóng help dialog.
|
| 895 |
+
*/
|
| 896 |
+
function closeHelpDialog() {
|
| 897 |
+
var overlay = document.getElementById('help-overlay');
|
| 898 |
+
if (!overlay) return;
|
| 899 |
+
overlay.classList.add('hidden');
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// ============================================================
|
| 903 |
+
// Khởi tạo ứng dụng — Main Initialization
|
| 904 |
+
// ============================================================
|
| 905 |
+
|
| 906 |
+
/**
|
| 907 |
+
* Hàm khởi tạo chính của ứng dụng Hikari Chatbot.
|
| 908 |
+
* - Kiểm tra CDN RiveScript đã tải chưa
|
| 909 |
+
* - Khởi tạo bot với ngôn ngữ mặc định (tiếng Việt)
|
| 910 |
+
* - Hiển thị lời chào khởi tạo chứa tên "Hikari" và hướng dẫn sử dụng
|
| 911 |
+
* - Gắn event listener cho Language Selector, nút gửi, input Enter, nút macros, nút rules
|
| 912 |
+
* @returns {Promise<void>}
|
| 913 |
+
*/
|
| 914 |
+
async function initializeApp() {
|
| 915 |
+
// Kiểm tra CDN RiveScript
|
| 916 |
+
if (typeof RiveScript === 'undefined') {
|
| 917 |
+
showError('Chatbot hiện không khả dụng. Vui lòng kiểm tra kết nối mạng và tải lại trang.');
|
| 918 |
+
return;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
// Load brain files từ .rive (chỉ trong trình duyệt)
|
| 922 |
+
if (typeof loadAllBrains === 'function') {
|
| 923 |
+
await loadAllBrains();
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
// Load dữ liệu JSON (chỉ trong trình duyệt)
|
| 927 |
+
if (typeof loadAllData === 'function') {
|
| 928 |
+
await loadAllData();
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
// Load preprocessed similarity data (chỉ trong trình duyệt)
|
| 932 |
+
if (typeof loadPreprocessedData === 'function') {
|
| 933 |
+
await loadPreprocessedData();
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
// Khởi tạo bot với tiếng Việt (ngôn ngữ mặc định)
|
| 937 |
+
await initBot('vi');
|
| 938 |
+
|
| 939 |
+
// Hiển thị lời chào khởi tạo
|
| 940 |
+
if (bot) {
|
| 941 |
+
var greeting = await bot.reply(USERNAME, GREETING_TRIGGERS['vi']);
|
| 942 |
+
appendMessage(greeting, 'bot');
|
| 943 |
+
updateRulesList('vi');
|
| 944 |
+
updateMacrosList('vi');
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
// === Gắn Event Listeners ===
|
| 948 |
+
|
| 949 |
+
// Language Selector: change → changeLanguage
|
| 950 |
+
var langSelector = document.getElementById('language-selector');
|
| 951 |
+
if (langSelector) {
|
| 952 |
+
langSelector.addEventListener('change', function(e) {
|
| 953 |
+
changeLanguage(e.target.value);
|
| 954 |
+
});
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
// Nút gửi: click → sendMessage
|
| 958 |
+
var sendBtn = document.getElementById('send-button');
|
| 959 |
+
if (sendBtn) {
|
| 960 |
+
sendBtn.addEventListener('click', sendMessage);
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
// Input: keypress Enter → sendMessage
|
| 964 |
+
var msgInput = document.getElementById('message-input');
|
| 965 |
+
if (msgInput) {
|
| 966 |
+
msgInput.addEventListener('keypress', function(e) {
|
| 967 |
+
if (e.key === 'Enter') {
|
| 968 |
+
sendMessage();
|
| 969 |
+
}
|
| 970 |
+
});
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
// Nút macros: click → toggleMacrosPanel
|
| 974 |
+
var macrosBtn = document.getElementById('macros-button');
|
| 975 |
+
if (macrosBtn) {
|
| 976 |
+
macrosBtn.addEventListener('click', toggleMacrosPanel);
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
// Nút rules: click → toggleRulesPanel
|
| 980 |
+
var rulesBtn = document.getElementById('rules-button');
|
| 981 |
+
if (rulesBtn) {
|
| 982 |
+
rulesBtn.addEventListener('click', toggleRulesPanel);
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
// Nút help: click → openHelpDialog
|
| 986 |
+
var helpBtn = document.getElementById('help-button');
|
| 987 |
+
if (helpBtn) {
|
| 988 |
+
helpBtn.addEventListener('click', openHelpDialog);
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
// Help dialog: đóng khi click nút X
|
| 992 |
+
var helpCloseBtn = document.getElementById('help-close-button');
|
| 993 |
+
if (helpCloseBtn) {
|
| 994 |
+
helpCloseBtn.addEventListener('click', closeHelpDialog);
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
// Help dialog: đóng khi click overlay (bên ngoài dialog)
|
| 998 |
+
var helpOverlay = document.getElementById('help-overlay');
|
| 999 |
+
if (helpOverlay) {
|
| 1000 |
+
helpOverlay.addEventListener('click', function(e) {
|
| 1001 |
+
if (e.target === helpOverlay) {
|
| 1002 |
+
closeHelpDialog();
|
| 1003 |
+
}
|
| 1004 |
+
});
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
// Help dialog: đóng khi nhấn Escape
|
| 1008 |
+
document.addEventListener('keydown', function(e) {
|
| 1009 |
+
if (e.key === 'Escape') {
|
| 1010 |
+
closeHelpDialog();
|
| 1011 |
+
}
|
| 1012 |
+
});
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
// Tự động khởi tạo khi DOM sẵn sàng (chỉ trong trình duyệt, không chạy trong môi trường test)
|
| 1016 |
+
if (typeof document !== 'undefined' && typeof module === 'undefined') {
|
| 1017 |
+
if (document.readyState === 'loading') {
|
| 1018 |
+
document.addEventListener('DOMContentLoaded', initializeApp);
|
| 1019 |
+
} else {
|
| 1020 |
+
initializeApp();
|
| 1021 |
+
}
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
// ============================================================
|
| 1025 |
+
// Export cho testing (module.exports nếu chạy trong Node/Vitest)
|
| 1026 |
+
// ============================================================
|
| 1027 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 1028 |
+
module.exports = {
|
| 1029 |
+
validateMessage: validateMessage,
|
| 1030 |
+
linkifyText: linkifyText,
|
| 1031 |
+
removeVietnameseDiacritics: removeVietnameseDiacritics,
|
| 1032 |
+
normalizeInput: normalizeInput,
|
| 1033 |
+
scrollToBottom: scrollToBottom,
|
| 1034 |
+
appendMessage: appendMessage,
|
| 1035 |
+
showError: showError,
|
| 1036 |
+
calculateConfidence: calculateConfidence,
|
| 1037 |
+
getConfidenceClass: getConfidenceClass,
|
| 1038 |
+
BRAIN_DATA: BRAIN_DATA,
|
| 1039 |
+
initBot: initBot,
|
| 1040 |
+
changeLanguage: changeLanguage,
|
| 1041 |
+
GREETING_TRIGGERS: GREETING_TRIGGERS,
|
| 1042 |
+
sendMessage: sendMessage,
|
| 1043 |
+
getBotReply: getBotReply,
|
| 1044 |
+
extractWebSearchQuery: extractWebSearchQuery,
|
| 1045 |
+
showLoadingIndicator: showLoadingIndicator,
|
| 1046 |
+
hideLoadingIndicator: hideLoadingIndicator,
|
| 1047 |
+
callFallbackAPI: callFallbackAPI,
|
| 1048 |
+
levenshteinDistance: levenshteinDistance,
|
| 1049 |
+
jaccardSimilarity: jaccardSimilarity,
|
| 1050 |
+
cosineSimilarity: cosineSimilarity,
|
| 1051 |
+
cosineSimilarityTFIDF: cosineSimilarityTFIDF,
|
| 1052 |
+
synsetSimilarity: synsetSimilarity,
|
| 1053 |
+
synsetSimilarityPreprocessed: synsetSimilarityPreprocessed,
|
| 1054 |
+
areSynonyms: areSynonyms,
|
| 1055 |
+
SYNONYM_GROUPS: SYNONYM_GROUPS,
|
| 1056 |
+
textSimilarity: textSimilarity,
|
| 1057 |
+
loadPreprocessedData: loadPreprocessedData,
|
| 1058 |
+
getPreprocessedLang: getPreprocessedLang,
|
| 1059 |
+
findBestMatchPreprocessed: findBestMatchPreprocessed,
|
| 1060 |
+
tokenizeForSimilarity: tokenizeForSimilarity,
|
| 1061 |
+
SPECIFIC_RESPONSES: SPECIFIC_RESPONSES,
|
| 1062 |
+
specificResponseAdapter: specificResponseAdapter,
|
| 1063 |
+
TIME_KEYWORDS: TIME_KEYWORDS,
|
| 1064 |
+
DAY_NAMES: DAY_NAMES,
|
| 1065 |
+
MONTH_NAMES_EN: MONTH_NAMES_EN,
|
| 1066 |
+
timeAdapter: timeAdapter,
|
| 1067 |
+
formatTime: formatTime,
|
| 1068 |
+
formatDate: formatDate,
|
| 1069 |
+
formatDay: formatDay,
|
| 1070 |
+
parseMathExpression: parseMathExpression,
|
| 1071 |
+
mathematicalEvaluationAdapter: mathematicalEvaluationAdapter,
|
| 1072 |
+
CONVERSION_FACTORS: CONVERSION_FACTORS,
|
| 1073 |
+
TEMPERATURE_UNITS: TEMPERATURE_UNITS,
|
| 1074 |
+
convertTemperature: convertTemperature,
|
| 1075 |
+
convertUnit: convertUnit,
|
| 1076 |
+
parseConversionRequest: parseConversionRequest,
|
| 1077 |
+
getSupportedUnits: getSupportedUnits,
|
| 1078 |
+
unitConversionAdapter: unitConversionAdapter,
|
| 1079 |
+
QA_DATASET: QA_DATASET,
|
| 1080 |
+
bestMatchAdapter: bestMatchAdapter,
|
| 1081 |
+
INVALID_RESPONSE_PHRASES: INVALID_RESPONSE_PHRASES,
|
| 1082 |
+
isValidAdapterResult: isValidAdapterResult,
|
| 1083 |
+
logicAdapterDispatcher: logicAdapterDispatcher,
|
| 1084 |
+
ADAPTER_REGISTRY: ADAPTER_REGISTRY,
|
| 1085 |
+
ADAPTER_DISPLAY_NAMES: ADAPTER_DISPLAY_NAMES,
|
| 1086 |
+
getAdapterDisplayName: getAdapterDisplayName,
|
| 1087 |
+
get _adapterPath() { return _adapterPath; },
|
| 1088 |
+
set _adapterPath(val) { _adapterPath = val; },
|
| 1089 |
+
registerAdapters: registerAdapters,
|
| 1090 |
+
webSearchAdapter: webSearchAdapter,
|
| 1091 |
+
googleSearch: googleSearch,
|
| 1092 |
+
duckDuckGoSearch: duckDuckGoSearch,
|
| 1093 |
+
toggleMacrosPanel: toggleMacrosPanel,
|
| 1094 |
+
updateMacrosList: updateMacrosList,
|
| 1095 |
+
toggleRulesPanel: toggleRulesPanel,
|
| 1096 |
+
updateRulesList: updateRulesList,
|
| 1097 |
+
formatTrigger: formatTrigger,
|
| 1098 |
+
initializeApp: initializeApp,
|
| 1099 |
+
// Expose state for testing
|
| 1100 |
+
get bot() { return bot; },
|
| 1101 |
+
set bot(val) { bot = val; },
|
| 1102 |
+
get currentLang() { return currentLang; },
|
| 1103 |
+
set currentLang(val) { currentLang = val; },
|
| 1104 |
+
USERNAME: USERNAME,
|
| 1105 |
+
FALLBACK_API_URL: FALLBACK_API_URL,
|
| 1106 |
+
FALLBACK_API_TIMEOUT: FALLBACK_API_TIMEOUT
|
| 1107 |
+
};
|
| 1108 |
+
}
|
brain.js
ADDED
|
@@ -0,0 +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 |
+
}
|
brain/en.rive
ADDED
|
@@ -0,0 +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? 🤔
|
brain/ja.rive
ADDED
|
@@ -0,0 +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 |
+
- すみません、よく分かりませんでした。別の聞き方を試してみてください 🤔
|
brain/vi.rive
ADDED
|
@@ -0,0 +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é! 🤔
|
data-loader.js
ADDED
|
@@ -0,0 +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 |
+
}
|
data/adapter-registry.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
data/help-content.json
ADDED
|
@@ -0,0 +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 |
+
}
|
data/preprocessed.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/qa-dataset.json
ADDED
|
@@ -0,0 +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 |
+
}
|
data/specific-responses.json
ADDED
|
@@ -0,0 +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 |
+
}
|
index.html
CHANGED
|
@@ -1,19 +1,76 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
<button id="help-button" title="Hướng dẫn sử dụng">❓</button>
|
| 16 |
+
<button id="rules-button" title="Xem danh sách rules">📋</button>
|
| 17 |
+
<button id="macros-button" title="Xem danh sách Object Macros">🔧</button>
|
| 18 |
+
<select id="language-selector">
|
| 19 |
+
<option value="vi" selected>Tiếng Việt</option>
|
| 20 |
+
<option value="en">English</option>
|
| 21 |
+
<option value="ja">日本語</option>
|
| 22 |
+
</select>
|
| 23 |
+
</div>
|
| 24 |
+
</header>
|
| 25 |
+
<div id="rules-panel" class="slide-panel hidden">
|
| 26 |
+
<div class="slide-panel-header">
|
| 27 |
+
<h3 id="rules-title">📋 Các câu hỏi Hikari có thể trả lời</h3>
|
| 28 |
+
<button class="slide-panel-close" onclick="toggleRulesPanel()" aria-label="Đóng">×</button>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="slide-panel-body">
|
| 31 |
+
<ul id="rules-list" class="rules-list"></ul>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
<div id="macros-panel" class="slide-panel hidden">
|
| 35 |
+
<div class="slide-panel-header">
|
| 36 |
+
<h3 id="macros-title">🔧 Object Macros (Subroutines)</h3>
|
| 37 |
+
<button class="slide-panel-close" onclick="toggleMacrosPanel()" aria-label="Đóng">×</button>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="slide-panel-body">
|
| 40 |
+
<ul id="macros-list" class="macros-list"></ul>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div id="message-display" class="message-display"></div>
|
| 44 |
+
<div class="input-area">
|
| 45 |
+
<input type="text" id="message-input" placeholder="Nhập tin nhắn...">
|
| 46 |
+
<button id="send-button">Gửi</button>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
<!-- Help Dialog -->
|
| 50 |
+
<div id="help-overlay" class="help-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="help-dialog-title">
|
| 51 |
+
<div class="help-dialog">
|
| 52 |
+
<div class="help-dialog-header">
|
| 53 |
+
<h2 id="help-dialog-title">📖 Hướng dẫn sử dụng</h2>
|
| 54 |
+
<button id="help-close-button" class="help-close-button" aria-label="Đóng">×</button>
|
| 55 |
+
</div>
|
| 56 |
+
<div id="help-dialog-body" class="help-dialog-body">
|
| 57 |
+
<!-- Nội dung được render bởi JS theo ngôn ngữ -->
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<script src="https://unpkg.com/rivescript@latest/dist/rivescript.min.js"></script>
|
| 63 |
+
<script src="brain.js"></script>
|
| 64 |
+
<script src="data-loader.js"></script>
|
| 65 |
+
<script src="adapters/text-similarity.js"></script>
|
| 66 |
+
<script src="adapters/specific-response.js"></script>
|
| 67 |
+
<script src="adapters/time-adapter.js"></script>
|
| 68 |
+
<script src="adapters/math-adapter.js"></script>
|
| 69 |
+
<script src="adapters/unit-conversion.js"></script>
|
| 70 |
+
<script src="adapters/best-match.js"></script>
|
| 71 |
+
<script src="adapters/logic-dispatcher.js"></script>
|
| 72 |
+
<script src="adapters/web-search.js"></script>
|
| 73 |
+
<script src="adapters/adapter-registry.js"></script>
|
| 74 |
+
<script src="app.js"></script>
|
| 75 |
+
</body>
|
| 76 |
+
</html>
|
style.css
CHANGED
|
@@ -1,28 +1,595 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
#language-selector option {
|
| 77 |
+
color: #333333;
|
| 78 |
+
background-color: #ffffff;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* === Slide Panel (Rules & Macros) === */
|
| 82 |
+
.slide-panel {
|
| 83 |
+
border-bottom: 1px solid #e0e0e0;
|
| 84 |
+
background-color: #f8f9fa;
|
| 85 |
+
max-height: 280px;
|
| 86 |
+
display: flex;
|
| 87 |
+
flex-direction: column;
|
| 88 |
+
animation: slideDown 0.2s ease-out;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@keyframes slideDown {
|
| 92 |
+
from { max-height: 0; opacity: 0; }
|
| 93 |
+
to { max-height: 280px; opacity: 1; }
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.slide-panel-header {
|
| 97 |
+
display: flex;
|
| 98 |
+
align-items: center;
|
| 99 |
+
justify-content: space-between;
|
| 100 |
+
padding: 10px 16px;
|
| 101 |
+
background-color: #eef2f7;
|
| 102 |
+
border-bottom: 1px solid #dde3ea;
|
| 103 |
+
flex-shrink: 0;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.slide-panel-header h3 {
|
| 107 |
+
font-size: 0.88rem;
|
| 108 |
+
font-weight: 600;
|
| 109 |
+
color: #4a90d9;
|
| 110 |
+
margin: 0;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.slide-panel-close {
|
| 114 |
+
background: none;
|
| 115 |
+
border: none;
|
| 116 |
+
font-size: 1.2rem;
|
| 117 |
+
color: #888888;
|
| 118 |
+
cursor: pointer;
|
| 119 |
+
padding: 0 4px;
|
| 120 |
+
line-height: 1;
|
| 121 |
+
border-radius: 4px;
|
| 122 |
+
transition: color 0.2s, background-color 0.2s;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.slide-panel-close:hover {
|
| 126 |
+
color: #333333;
|
| 127 |
+
background-color: rgba(0, 0, 0, 0.08);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.slide-panel-body {
|
| 131 |
+
overflow-y: auto;
|
| 132 |
+
padding: 8px 16px 12px;
|
| 133 |
+
flex: 1;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/* === Rules List === */
|
| 137 |
+
.rules-list {
|
| 138 |
+
list-style: none;
|
| 139 |
+
padding: 0;
|
| 140 |
+
margin: 0;
|
| 141 |
+
display: flex;
|
| 142 |
+
flex-wrap: wrap;
|
| 143 |
+
gap: 6px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.rules-list li {
|
| 147 |
+
display: inline-block;
|
| 148 |
+
padding: 4px 10px;
|
| 149 |
+
font-size: 0.82rem;
|
| 150 |
+
color: #4a6785;
|
| 151 |
+
background-color: #e8eef5;
|
| 152 |
+
border-radius: 12px;
|
| 153 |
+
border: 1px solid #d0dae6;
|
| 154 |
+
cursor: default;
|
| 155 |
+
transition: background-color 0.15s;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.rules-list li:hover {
|
| 159 |
+
background-color: #dce4ee;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* === Macros List === */
|
| 163 |
+
.macros-list {
|
| 164 |
+
list-style: none;
|
| 165 |
+
padding: 0;
|
| 166 |
+
margin: 0;
|
| 167 |
+
display: flex;
|
| 168 |
+
flex-direction: column;
|
| 169 |
+
gap: 8px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.macro-item {
|
| 173 |
+
padding: 10px 12px;
|
| 174 |
+
background-color: #ffffff;
|
| 175 |
+
border-radius: 8px;
|
| 176 |
+
border: 1px solid #e0e5ec;
|
| 177 |
+
transition: box-shadow 0.15s;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.macro-item:hover {
|
| 181 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.macro-item-header {
|
| 185 |
+
display: flex;
|
| 186 |
+
align-items: center;
|
| 187 |
+
justify-content: space-between;
|
| 188 |
+
margin-bottom: 4px;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.macro-item strong {
|
| 192 |
+
font-size: 0.85rem;
|
| 193 |
+
color: #333333;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.macro-item-badge {
|
| 197 |
+
display: inline-block;
|
| 198 |
+
font-size: 0.68rem;
|
| 199 |
+
padding: 1px 6px;
|
| 200 |
+
border-radius: 8px;
|
| 201 |
+
background-color: #d4edda;
|
| 202 |
+
color: #28a745;
|
| 203 |
+
font-weight: 500;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.macro-item p {
|
| 207 |
+
font-size: 0.8rem;
|
| 208 |
+
color: #666666;
|
| 209 |
+
margin: 2px 0 6px;
|
| 210 |
+
line-height: 1.4;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.macro-call-syntax {
|
| 214 |
+
display: inline-block;
|
| 215 |
+
font-family: 'Courier New', Courier, monospace;
|
| 216 |
+
font-size: 0.76rem;
|
| 217 |
+
background-color: #f0f0f0;
|
| 218 |
+
color: #c7254e;
|
| 219 |
+
padding: 3px 8px;
|
| 220 |
+
border-radius: 4px;
|
| 221 |
+
border: 1px solid #e0e0e0;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/* === Message Display === */
|
| 225 |
+
.message-display {
|
| 226 |
+
flex: 1;
|
| 227 |
+
overflow-y: auto;
|
| 228 |
+
padding: 16px;
|
| 229 |
+
display: flex;
|
| 230 |
+
flex-direction: column;
|
| 231 |
+
gap: 10px;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* === Messages === */
|
| 235 |
+
.message {
|
| 236 |
+
max-width: 80%;
|
| 237 |
+
padding: 10px 14px;
|
| 238 |
+
border-radius: 12px;
|
| 239 |
+
word-wrap: break-word;
|
| 240 |
+
line-height: 1.45;
|
| 241 |
+
font-size: 0.95rem;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.message.user {
|
| 245 |
+
align-self: flex-end;
|
| 246 |
+
background-color: #4a90d9;
|
| 247 |
+
color: #ffffff;
|
| 248 |
+
border-bottom-right-radius: 4px;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.message.bot {
|
| 252 |
+
align-self: flex-start;
|
| 253 |
+
background-color: #e9ecef;
|
| 254 |
+
color: #333333;
|
| 255 |
+
border-bottom-left-radius: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
/* === Confidence === */
|
| 259 |
+
.confidence {
|
| 260 |
+
display: block;
|
| 261 |
+
font-size: 0.72rem;
|
| 262 |
+
margin-top: 4px;
|
| 263 |
+
font-weight: 500;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.confidence-high {
|
| 267 |
+
color: #28a745;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.confidence-low {
|
| 271 |
+
color: #dc3545;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/* === Adapter Path Breadcrumb === */
|
| 275 |
+
.adapter-path {
|
| 276 |
+
display: block;
|
| 277 |
+
font-size: 0.7rem;
|
| 278 |
+
margin-top: 2px;
|
| 279 |
+
color: #8e8e8e;
|
| 280 |
+
font-style: italic;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
/* === Response Time === */
|
| 284 |
+
.response-time {
|
| 285 |
+
display: block;
|
| 286 |
+
font-size: 0.68rem;
|
| 287 |
+
margin-top: 2px;
|
| 288 |
+
color: #aaaaaa;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/* === Links in bot messages === */
|
| 292 |
+
.message.bot a {
|
| 293 |
+
color: #4a90d9;
|
| 294 |
+
text-decoration: underline;
|
| 295 |
+
word-break: break-all;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.message.bot a:hover {
|
| 299 |
+
color: #3a7bc8;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* === Loading Indicator === */
|
| 303 |
+
.loading-indicator {
|
| 304 |
+
align-self: flex-start;
|
| 305 |
+
padding: 10px 14px;
|
| 306 |
+
background-color: #e9ecef;
|
| 307 |
+
border-radius: 12px;
|
| 308 |
+
font-size: 0.9rem;
|
| 309 |
+
color: #888888;
|
| 310 |
+
animation: pulse 1.2s ease-in-out infinite;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
@keyframes pulse {
|
| 314 |
+
0%, 100% { opacity: 1; }
|
| 315 |
+
50% { opacity: 0.5; }
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* === Input Area === */
|
| 319 |
+
.input-area {
|
| 320 |
+
display: flex;
|
| 321 |
+
padding: 12px 16px;
|
| 322 |
+
border-top: 1px solid #e0e0e0;
|
| 323 |
+
background-color: #ffffff;
|
| 324 |
+
gap: 8px;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.input-area input {
|
| 328 |
+
flex: 1;
|
| 329 |
+
padding: 10px 14px;
|
| 330 |
+
border: 1px solid #cccccc;
|
| 331 |
+
border-radius: 20px;
|
| 332 |
+
font-size: 0.95rem;
|
| 333 |
+
outline: none;
|
| 334 |
+
transition: border-color 0.2s;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.input-area input:focus {
|
| 338 |
+
border-color: #4a90d9;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.input-area button {
|
| 342 |
+
padding: 10px 20px;
|
| 343 |
+
background-color: #4a90d9;
|
| 344 |
+
color: #ffffff;
|
| 345 |
+
border: none;
|
| 346 |
+
border-radius: 20px;
|
| 347 |
+
font-size: 0.95rem;
|
| 348 |
+
cursor: pointer;
|
| 349 |
+
transition: background-color 0.2s;
|
| 350 |
+
white-space: nowrap;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.input-area button:hover {
|
| 354 |
+
background-color: #3a7bc8;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* === Utility === */
|
| 358 |
+
.hidden {
|
| 359 |
+
display: none !important;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
/* === Responsive — Mobile (max-width: 768px, min-width: 320px) === */
|
| 363 |
+
@media (max-width: 768px) {
|
| 364 |
+
body {
|
| 365 |
+
padding: 0;
|
| 366 |
+
align-items: stretch;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.chat-container {
|
| 370 |
+
max-width: 100%;
|
| 371 |
+
height: 100vh;
|
| 372 |
+
border-radius: 0;
|
| 373 |
+
box-shadow: none;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.chat-header {
|
| 377 |
+
padding: 10px 12px;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.chat-header h1 {
|
| 381 |
+
font-size: 1.1rem;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.header-controls {
|
| 385 |
+
gap: 6px;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
#language-selector {
|
| 389 |
+
font-size: 0.8rem;
|
| 390 |
+
padding: 3px 6px;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.message-display {
|
| 394 |
+
padding: 12px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.message {
|
| 398 |
+
max-width: 88%;
|
| 399 |
+
font-size: 0.9rem;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.input-area {
|
| 403 |
+
padding: 10px 12px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.input-area input {
|
| 407 |
+
padding: 8px 12px;
|
| 408 |
+
font-size: 0.9rem;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.input-area button {
|
| 412 |
+
padding: 8px 16px;
|
| 413 |
+
font-size: 0.9rem;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.rules-panel,
|
| 417 |
+
.macros-panel,
|
| 418 |
+
.slide-panel {
|
| 419 |
+
max-height: 200px;
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
/* === Help Dialog Overlay === */
|
| 424 |
+
.help-overlay {
|
| 425 |
+
position: fixed;
|
| 426 |
+
top: 0;
|
| 427 |
+
left: 0;
|
| 428 |
+
width: 100%;
|
| 429 |
+
height: 100%;
|
| 430 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 431 |
+
display: flex;
|
| 432 |
+
justify-content: center;
|
| 433 |
+
align-items: center;
|
| 434 |
+
z-index: 1000;
|
| 435 |
+
animation: fadeIn 0.2s ease-out;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
@keyframes fadeIn {
|
| 439 |
+
from { opacity: 0; }
|
| 440 |
+
to { opacity: 1; }
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.help-dialog {
|
| 444 |
+
background-color: #ffffff;
|
| 445 |
+
border-radius: 12px;
|
| 446 |
+
width: 90%;
|
| 447 |
+
max-width: 520px;
|
| 448 |
+
max-height: 80vh;
|
| 449 |
+
display: flex;
|
| 450 |
+
flex-direction: column;
|
| 451 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
| 452 |
+
animation: slideUp 0.25s ease-out;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
@keyframes slideUp {
|
| 456 |
+
from { transform: translateY(24px); opacity: 0; }
|
| 457 |
+
to { transform: translateY(0); opacity: 1; }
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.help-dialog-header {
|
| 461 |
+
display: flex;
|
| 462 |
+
align-items: center;
|
| 463 |
+
justify-content: space-between;
|
| 464 |
+
padding: 16px 20px;
|
| 465 |
+
border-bottom: 1px solid #e0e0e0;
|
| 466 |
+
background: linear-gradient(135deg, #4a90d9, #5ba3e6);
|
| 467 |
+
color: #ffffff;
|
| 468 |
+
border-radius: 12px 12px 0 0;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.help-dialog-header h2 {
|
| 472 |
+
font-size: 1.05rem;
|
| 473 |
+
font-weight: 600;
|
| 474 |
+
margin: 0;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.help-close-button {
|
| 478 |
+
background: none;
|
| 479 |
+
border: none;
|
| 480 |
+
color: #ffffff;
|
| 481 |
+
font-size: 1.5rem;
|
| 482 |
+
cursor: pointer;
|
| 483 |
+
padding: 0 4px;
|
| 484 |
+
line-height: 1;
|
| 485 |
+
border-radius: 4px;
|
| 486 |
+
transition: background-color 0.2s;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.help-close-button:hover {
|
| 490 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
.help-dialog-body {
|
| 494 |
+
padding: 20px;
|
| 495 |
+
overflow-y: auto;
|
| 496 |
+
font-size: 0.9rem;
|
| 497 |
+
line-height: 1.6;
|
| 498 |
+
color: #333333;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.help-section {
|
| 502 |
+
margin-bottom: 18px;
|
| 503 |
+
padding-bottom: 16px;
|
| 504 |
+
border-bottom: 1px solid #f0f0f0;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.help-section:last-child {
|
| 508 |
+
margin-bottom: 0;
|
| 509 |
+
padding-bottom: 0;
|
| 510 |
+
border-bottom: none;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.help-section h3 {
|
| 514 |
+
font-size: 0.92rem;
|
| 515 |
+
font-weight: 600;
|
| 516 |
+
color: #4a90d9;
|
| 517 |
+
margin-bottom: 8px;
|
| 518 |
+
display: flex;
|
| 519 |
+
align-items: center;
|
| 520 |
+
gap: 6px;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.help-section p {
|
| 524 |
+
margin: 4px 0;
|
| 525 |
+
color: #555555;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.help-section ul {
|
| 529 |
+
list-style: none;
|
| 530 |
+
padding: 0;
|
| 531 |
+
margin: 6px 0 0 0;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.help-section li {
|
| 535 |
+
padding: 4px 0 4px 16px;
|
| 536 |
+
position: relative;
|
| 537 |
+
color: #555555;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.help-section li::before {
|
| 541 |
+
content: '•';
|
| 542 |
+
position: absolute;
|
| 543 |
+
left: 4px;
|
| 544 |
+
color: #4a90d9;
|
| 545 |
+
font-weight: bold;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.help-example {
|
| 549 |
+
display: inline-block;
|
| 550 |
+
background-color: #f0f4f8;
|
| 551 |
+
color: #4a90d9;
|
| 552 |
+
padding: 2px 8px;
|
| 553 |
+
border-radius: 4px;
|
| 554 |
+
font-size: 0.84rem;
|
| 555 |
+
font-family: 'Courier New', Courier, monospace;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.help-confidence-demo {
|
| 559 |
+
display: flex;
|
| 560 |
+
gap: 12px;
|
| 561 |
+
margin-top: 8px;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.help-confidence-item {
|
| 565 |
+
display: flex;
|
| 566 |
+
align-items: center;
|
| 567 |
+
gap: 6px;
|
| 568 |
+
font-size: 0.84rem;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.help-dot {
|
| 572 |
+
width: 10px;
|
| 573 |
+
height: 10px;
|
| 574 |
+
border-radius: 50%;
|
| 575 |
+
display: inline-block;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.help-dot.green { background-color: #28a745; }
|
| 579 |
+
.help-dot.red { background-color: #dc3545; }
|
| 580 |
+
|
| 581 |
+
/* === Responsive — Help Dialog Mobile === */
|
| 582 |
+
@media (max-width: 768px) {
|
| 583 |
+
.help-dialog {
|
| 584 |
+
width: 95%;
|
| 585 |
+
max-height: 85vh;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.help-dialog-header {
|
| 589 |
+
padding: 12px 16px;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.help-dialog-body {
|
| 593 |
+
padding: 16px;
|
| 594 |
+
}
|
| 595 |
+
}
|