mr4 commited on
Commit
14b52eb
·
verified ·
1 Parent(s): 55cb5f9

Upload 23 files

Browse files
.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, '&amp;')
142
+ .replace(/</g, '&lt;')
143
+ .replace(/>/g, '&gt;')
144
+ .replace(/"/g, '&quot;');
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>Đỏ (&lt;50%)</b>: Hikari không chắc chắn, sẽ thử hỏi dịch vụ bổ sung."
22
+ ]
23
+ },
24
+ {
25
+ "icon": "⚡",
26
+ "heading": "Các lệnh đặc biệt",
27
+ "items": [
28
+ "<span class=\"help-example\">tính 2 + 3</span> — Tính toán biểu thức toán học",
29
+ "<span class=\"help-example\">đổi 5 km sang m</span> — Chuyển đổi đơn vị",
30
+ "<span class=\"help-example\">mấy giờ</span> — Xem giờ hiện tại",
31
+ "<span class=\"help-example\">hôm nay ngày mấy</span> — Xem ngày tháng",
32
+ "<span class=\"help-example\">tìm kiếm ...</span> — Tìm câu trả lời phù hợp nhất"
33
+ ]
34
+ },
35
+ {
36
+ "icon": "📋",
37
+ "heading": "Mẹo sử dụng",
38
+ "items": [
39
+ "Nhấn nút <b>📋</b> để xem danh sách tất cả câu hỏi Hikari có thể trả lời.",
40
+ "Nhấn nút <b>🔧</b> để xem danh sách các chức năng mở rộng (Object Macros).",
41
+ "Nếu Hikari không hiểu, hãy thử diễn đạt lại câu hỏi đơn giản hơn."
42
+ ]
43
+ }
44
+ ]
45
+ },
46
+ "en": {
47
+ "title": "📖 User Guide",
48
+ "sections": [
49
+ {
50
+ "icon": "💬",
51
+ "heading": "How to chat with Hikari",
52
+ "items": [
53
+ "Type your message in the input box below and press <b>Send</b> or hit <b>Enter</b>.",
54
+ "Hikari supports 3 languages: Tiếng Việt, English, 日本語. Select from the dropdown in the top right.",
55
+ "You can ask anything — greetings, knowledge, math, unit conversion, time..."
56
+ ]
57
+ },
58
+ {
59
+ "icon": "🤖",
60
+ "heading": "How Hikari responds",
61
+ "items": [
62
+ "Hikari matches your message against pre-programmed <b>triggers</b>.",
63
+ "Each response includes a <b>Confidence</b> score showing how well it matched.",
64
+ "<span class=\"help-dot green\"></span> <b>Green (≥50%)</b>: Hikari is confident in the answer.",
65
+ "<span class=\"help-dot red\"></span> <b>Red (&lt;50%)</b>: Hikari is unsure and will try a fallback service."
66
+ ]
67
+ },
68
+ {
69
+ "icon": "⚡",
70
+ "heading": "Special commands",
71
+ "items": [
72
+ "<span class=\"help-example\">calculate 2 + 3</span> — Evaluate math expressions",
73
+ "<span class=\"help-example\">convert 5 km to m</span> — Convert units",
74
+ "<span class=\"help-example\">what time is it</span> — Check current time",
75
+ "<span class=\"help-example\">what date is it</span> — Check today's date",
76
+ "<span class=\"help-example\">search ...</span> — Find the best matching answer"
77
+ ]
78
+ },
79
+ {
80
+ "icon": "📋",
81
+ "heading": "Tips",
82
+ "items": [
83
+ "Click <b>📋</b> to see all questions Hikari can answer.",
84
+ "Click <b>🔧</b> to see available extended features (Object Macros).",
85
+ "If Hikari doesn't understand, try rephrasing your question more simply."
86
+ ]
87
+ }
88
+ ]
89
+ },
90
+ "ja": {
91
+ "title": "📖 使い方ガイド",
92
+ "sections": [
93
+ {
94
+ "icon": "💬",
95
+ "heading": "ひかりとのチャット方法",
96
+ "items": [
97
+ "下の入力欄にメッセージを入力し、<b>送信</b>ボタンまたは<b>Enter</b>キーを押してください。",
98
+ "ひかりは3つの言語に対応:Tiếng Việt、English、日本語。右上のドロップダウンで選択できます。",
99
+ "何でも聞いてください — 挨拶、知識、計算、単位変換、時間確認..."
100
+ ]
101
+ },
102
+ {
103
+ "icon": "🤖",
104
+ "heading": "ひかりの応答方法",
105
+ "items": [
106
+ "ひかりはメッセージをプログラムされた<b>トリガー</b>と照合します。",
107
+ "各応答には一致度を示す<b>Confidence</b>(信頼度)スコアが付きます。",
108
+ "<span class=\"help-dot green\"></span> <b>緑(≥50%)</b>:ひかりは回答に自信があります。",
109
+ "<span class=\"help-dot red\"></span> <b>赤(&lt;50%)</b>:ひかりは不確かで、フォールバックサービスを試みます。"
110
+ ]
111
+ },
112
+ {
113
+ "icon": "⚡",
114
+ "heading": "特別なコマンド",
115
+ "items": [
116
+ "<span class=\"help-example\">計算 2 + 3</span> — 数式を計算",
117
+ "<span class=\"help-example\">変換 5 km を m</span> — 単位を変換",
118
+ "<span class=\"help-example\">今何時</span> — 現在の時刻を確認",
119
+ "<span class=\"help-example\">今日は何日</span> — 今日の日付を確認",
120
+ "<span class=\"help-example\">検索 ...</span> — 最適な回答を検索"
121
+ ]
122
+ },
123
+ {
124
+ "icon": "📋",
125
+ "heading": "ヒント",
126
+ "items": [
127
+ "<b>📋</b>ボタンでひかりが答えられる質問一覧を表示。",
128
+ "<b>🔧</b>ボタンで拡張機能(Object Macros)一覧を表示。",
129
+ "ひかりが理解できない場合は、より簡単な表現で質問してみてください。"
130
+ ]
131
+ }
132
+ ]
133
+ }
134
+ }
data/preprocessed.json 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
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Hikari Chatbot</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌟</text></svg>">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div class="chat-container">
12
+ <header class="chat-header">
13
+ <h1>Hikari</h1>
14
+ <div class="header-controls">
15
+ <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">&times;</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">&times;</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">&times;</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
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
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
+ }