@kavchat/core 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -80,11 +80,16 @@ type ClientRelease = {
80
80
  * - אותיות קטנות, הסרת ניקוד וטעמים (עברית), תשכיל (ערבית) וסימני הטעמה לטיניים (é → e)
81
81
  * - אותיות סופיות בעברית (ם → מ), צורות אליף בערבית (أ → ا), ё → е, ß → ss
82
82
  * - מילות קישור לכל שפה, והסרת תחיליות בעברית (ו, ה, ב...) ובערבית (ال, و, ب...)
83
+ * - תאית ושאר הכתבים בלי רווחים בין מילים (לאו, חמרית, בורמזית, יפנית וסינית): ראו isSpacelessScript
83
84
  *
84
85
  * הפורטים ל-Dart, Swift ו-Kotlin חייבים לעבור את test-vectors/normalize.json
85
86
  */
86
- declare const SUPPORTED_LOCALES: readonly ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr"];
87
+ declare const SUPPORTED_LOCALES: readonly ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr", "th"];
87
88
  type Locale = (typeof SUPPORTED_LOCALES)[number];
89
+ /**
90
+ * מילה שיש בה תו מכתב בלי רווחים. מילות מפתח כאלה מותאמות כתת-מחרוזת של השאלה, ולא כמילה שלמה (ראו match.ts)
91
+ */
92
+ declare function isSpacelessScript(word: string): boolean;
88
93
  /** מנקה ומחזיר רשימת מילים */
89
94
  declare function normalizeWords(text: string): string[];
90
95
  /**
@@ -110,6 +115,13 @@ declare const toBubbles: (text: string) => string[];
110
115
  * התאמה של שאלה חופשית לשלב בתסריט
111
116
  * ציון: מילה מובילה = 3, שם התסריט או כפתור שמוביל לשלב = 2, שגיאת כתיב של אות אחת = 0.5.
112
117
  * שלב אחד עם ציון כפול מהבא = single. כמה קרובים = multi (עד 3). אין כלום = none
118
+ *
119
+ * כתבים בלי רווחים (תאית, לאו, חמרית, בורמזית, יפנית, סינית): "ใบเสร็จอยู่ที่ไหน" היא מילה אחת בעיני normalizeWords,
120
+ * ולכן השוואת מילים שלמות לא תמצא בה את "ใบเสร็จ". מילת מפתח, שם תסריט או כפתור שיש בהם תו מכתב כזה
121
+ * מותאמים כתת-מחרוזת של כל השאלה המנורמלת, אחרי חיבור המילים בלי רווחים (משתמשים מוסיפים רווחים באמצע).
122
+ * המשקלות זהים (3 ו-2). בתוך שלב, מונח שמוכל במונח ארוך יותר שנמצא, עם משקל שווה או גבוה, לא נספר שוב.
123
+ * אין סובלנות לשגיאות כתיב בכתבים האלה: השאלה היא רצף אחד בלי גבולות מילים, ושינוי של סימן אחד
124
+ * (למשל טון: ข้าว ו-ข่าว) הוא מילה אחרת. שגיאה של אות אחת בתת-מחרוזת קצרה הייתה מתאימה כמעט לכל שאלה
113
125
  */
114
126
  type MatchReason = {
115
127
  kind: 'keyword' | 'title' | 'typo';
@@ -307,5 +319,5 @@ declare function eventsPayload(state: BotState, sessionId: string, releaseVersio
307
319
  }[];
308
320
  };
309
321
 
310
- export { AI_ANSWER_MAX, AI_QUESTION_MAX, RTL_LOCALES, SUPPORTED_LOCALES, UI_TEXTS, aiQuestion, answerWithAi, configureEngine, eventsPayload, format, groupKey, isRtl, keyTerms, matchQuestion, normalizeWords, pendingQuestion, pressChip, sendText, startConversation, stripPrefixes, toBubbles, transcriptOf, uiTexts, withinOneEdit };
322
+ export { AI_ANSWER_MAX, AI_QUESTION_MAX, RTL_LOCALES, SUPPORTED_LOCALES, UI_TEXTS, aiQuestion, answerWithAi, configureEngine, eventsPayload, format, groupKey, isRtl, isSpacelessScript, keyTerms, matchQuestion, normalizeWords, pendingQuestion, pressChip, sendText, startConversation, stripPrefixes, toBubbles, transcriptOf, uiTexts, withinOneEdit };
311
323
  export type { BotAction, BotButton, BotChip, BotEffect, BotMatch, BotMatchResult, BotMessage, BotOutcome, BotPathEntry, BotPosition, BotResult, BotScript, BotScriptContent, BotSettings, BotState, BotStep, ChipKind, ClientRelease, EngineOptions, Locale, MatchReason, TranscriptEntry, UiKey, UiTexts };
package/dist/index.d.ts CHANGED
@@ -80,11 +80,16 @@ type ClientRelease = {
80
80
  * - אותיות קטנות, הסרת ניקוד וטעמים (עברית), תשכיל (ערבית) וסימני הטעמה לטיניים (é → e)
81
81
  * - אותיות סופיות בעברית (ם → מ), צורות אליף בערבית (أ → ا), ё → е, ß → ss
82
82
  * - מילות קישור לכל שפה, והסרת תחיליות בעברית (ו, ה, ב...) ובערבית (ال, و, ب...)
83
+ * - תאית ושאר הכתבים בלי רווחים בין מילים (לאו, חמרית, בורמזית, יפנית וסינית): ראו isSpacelessScript
83
84
  *
84
85
  * הפורטים ל-Dart, Swift ו-Kotlin חייבים לעבור את test-vectors/normalize.json
85
86
  */
86
- declare const SUPPORTED_LOCALES: readonly ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr"];
87
+ declare const SUPPORTED_LOCALES: readonly ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr", "th"];
87
88
  type Locale = (typeof SUPPORTED_LOCALES)[number];
89
+ /**
90
+ * מילה שיש בה תו מכתב בלי רווחים. מילות מפתח כאלה מותאמות כתת-מחרוזת של השאלה, ולא כמילה שלמה (ראו match.ts)
91
+ */
92
+ declare function isSpacelessScript(word: string): boolean;
88
93
  /** מנקה ומחזיר רשימת מילים */
89
94
  declare function normalizeWords(text: string): string[];
90
95
  /**
@@ -110,6 +115,13 @@ declare const toBubbles: (text: string) => string[];
110
115
  * התאמה של שאלה חופשית לשלב בתסריט
111
116
  * ציון: מילה מובילה = 3, שם התסריט או כפתור שמוביל לשלב = 2, שגיאת כתיב של אות אחת = 0.5.
112
117
  * שלב אחד עם ציון כפול מהבא = single. כמה קרובים = multi (עד 3). אין כלום = none
118
+ *
119
+ * כתבים בלי רווחים (תאית, לאו, חמרית, בורמזית, יפנית, סינית): "ใบเสร็จอยู่ที่ไหน" היא מילה אחת בעיני normalizeWords,
120
+ * ולכן השוואת מילים שלמות לא תמצא בה את "ใบเสร็จ". מילת מפתח, שם תסריט או כפתור שיש בהם תו מכתב כזה
121
+ * מותאמים כתת-מחרוזת של כל השאלה המנורמלת, אחרי חיבור המילים בלי רווחים (משתמשים מוסיפים רווחים באמצע).
122
+ * המשקלות זהים (3 ו-2). בתוך שלב, מונח שמוכל במונח ארוך יותר שנמצא, עם משקל שווה או גבוה, לא נספר שוב.
123
+ * אין סובלנות לשגיאות כתיב בכתבים האלה: השאלה היא רצף אחד בלי גבולות מילים, ושינוי של סימן אחד
124
+ * (למשל טון: ข้าว ו-ข่าว) הוא מילה אחרת. שגיאה של אות אחת בתת-מחרוזת קצרה הייתה מתאימה כמעט לכל שאלה
113
125
  */
114
126
  type MatchReason = {
115
127
  kind: 'keyword' | 'title' | 'typo';
@@ -307,5 +319,5 @@ declare function eventsPayload(state: BotState, sessionId: string, releaseVersio
307
319
  }[];
308
320
  };
309
321
 
310
- export { AI_ANSWER_MAX, AI_QUESTION_MAX, RTL_LOCALES, SUPPORTED_LOCALES, UI_TEXTS, aiQuestion, answerWithAi, configureEngine, eventsPayload, format, groupKey, isRtl, keyTerms, matchQuestion, normalizeWords, pendingQuestion, pressChip, sendText, startConversation, stripPrefixes, toBubbles, transcriptOf, uiTexts, withinOneEdit };
322
+ export { AI_ANSWER_MAX, AI_QUESTION_MAX, RTL_LOCALES, SUPPORTED_LOCALES, UI_TEXTS, aiQuestion, answerWithAi, configureEngine, eventsPayload, format, groupKey, isRtl, isSpacelessScript, keyTerms, matchQuestion, normalizeWords, pendingQuestion, pressChip, sendText, startConversation, stripPrefixes, toBubbles, transcriptOf, uiTexts, withinOneEdit };
311
323
  export type { BotAction, BotButton, BotChip, BotEffect, BotMatch, BotMatchResult, BotMessage, BotOutcome, BotPathEntry, BotPosition, BotResult, BotScript, BotScriptContent, BotSettings, BotState, BotStep, ChipKind, ClientRelease, EngineOptions, Locale, MatchReason, TranscriptEntry, UiKey, UiTexts };
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  format: () => format,
33
33
  groupKey: () => groupKey,
34
34
  isRtl: () => isRtl,
35
+ isSpacelessScript: () => isSpacelessScript,
35
36
  keyTerms: () => keyTerms,
36
37
  matchQuestion: () => matchQuestion,
37
38
  normalizeWords: () => normalizeWords,
@@ -48,7 +49,7 @@ __export(index_exports, {
48
49
  module.exports = __toCommonJS(index_exports);
49
50
 
50
51
  // src/normalize.ts
51
- var SUPPORTED_LOCALES = ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr"];
52
+ var SUPPORTED_LOCALES = ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr", "th"];
52
53
  var FOLD = {
53
54
  // עברית: אותיות סופיות
54
55
  \u05DA: "\u05DB",
@@ -74,6 +75,13 @@ var FOLD = {
74
75
  function isCombiningMark(code) {
75
76
  return code >= 768 && code <= 879 || code >= 1425 && code <= 1479 || code >= 1552 && code <= 1562 || code >= 1611 && code <= 1631 || code === 1648 || code >= 1750 && code <= 1773;
76
77
  }
78
+ function isSpacelessLetter(code) {
79
+ return code >= 3585 && code <= 3630 || code >= 3632 && code <= 3642 || code >= 3648 && code <= 3653 || code >= 3655 && code <= 3662 || code >= 3713 && code <= 3758 || code >= 3760 && code <= 3781 || code >= 3784 && code <= 3790 || code >= 3804 && code <= 3807 || code >= 4096 && code <= 4169 || code >= 4176 && code <= 4253 || code >= 6016 && code <= 6099 || code === 6108 || code === 6109 || code === 12293 || code >= 12353 && code <= 12438 || code >= 12441 && code <= 12442 || code >= 12445 && code <= 12447 || code >= 12449 && code <= 12538 || code >= 12540 && code <= 12543 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959;
80
+ }
81
+ function isSpacelessScript(word) {
82
+ for (let i = 0; i < word.length; i++) if (isSpacelessLetter(word.charCodeAt(i))) return true;
83
+ return false;
84
+ }
77
85
  function isWordChar(ch) {
78
86
  const code = ch.charCodeAt(0);
79
87
  if (code >= 48 && code <= 57) return true;
@@ -81,9 +89,75 @@ function isWordChar(ch) {
81
89
  if (code >= 1488 && code <= 1514) return true;
82
90
  if (code >= 1568 && code <= 1610) return true;
83
91
  if (code >= 1649 && code <= 1747) return true;
84
- if (code >= 19968 && code <= 40959) return true;
92
+ if (isSpacelessLetter(code)) return true;
85
93
  return ch.toLowerCase() !== ch.toUpperCase();
86
94
  }
95
+ var isZeroWidth = (code) => code >= 8203 && code <= 8205 || code === 8288 || code === 65279;
96
+ var DIGIT_ZEROS = [3664, 3792, 4160, 6112];
97
+ function asciiDigit(code) {
98
+ for (const zero of DIGIT_ZEROS) if (code >= zero && code <= zero + 9) return String.fromCharCode(48 + code - zero);
99
+ return void 0;
100
+ }
101
+ var MARK_ORDER = {
102
+ 3640: 103,
103
+ 3641: 103,
104
+ 3642: 9,
105
+ 3656: 107,
106
+ 3657: 107,
107
+ 3658: 107,
108
+ 3659: 107,
109
+ 3768: 118,
110
+ 3769: 118,
111
+ 3770: 9,
112
+ 3784: 122,
113
+ 3785: 122,
114
+ 3786: 122,
115
+ 3787: 122,
116
+ 4151: 7,
117
+ 4153: 9,
118
+ 4154: 9,
119
+ 4237: 220,
120
+ 6098: 9,
121
+ 6109: 230,
122
+ 12441: 8,
123
+ 12442: 8
124
+ };
125
+ var isThaiTone = (code) => code >= 3656 && code <= 3659;
126
+ function orderMarks(text) {
127
+ if (!/[\u0e33\u0e38-\u0e3a\u0e48-\u0e4b\u0e4d\u0eb8-\u0eba\u0ec8-\u0ecb\u1037\u1039\u103a\u108d\u17d2\u17dd\u3099\u309a]/.test(text)) return text;
128
+ const units = [];
129
+ for (let i = 0; i < text.length; i++) units.push(text.charCodeAt(i));
130
+ for (let i = 0; i < units.length; i++) {
131
+ if (!MARK_ORDER[units[i]]) continue;
132
+ let end = i;
133
+ while (end < units.length && MARK_ORDER[units[end]]) end++;
134
+ for (let a = i + 1; a < end; a++) {
135
+ const v = units[a];
136
+ let b = a - 1;
137
+ while (b >= i && MARK_ORDER[units[b]] > MARK_ORDER[v]) {
138
+ units[b + 1] = units[b];
139
+ b--;
140
+ }
141
+ units[b + 1] = v;
142
+ }
143
+ i = end - 1;
144
+ }
145
+ let out = "";
146
+ for (let i = 0; i < units.length; i++) {
147
+ const c = units[i];
148
+ if (c === 3661 && units[i + 1] === 3634) {
149
+ out += "\u0E33";
150
+ i++;
151
+ } else if (c === 3661 && isThaiTone(units[i + 1]) && units[i + 2] === 3634) {
152
+ out += String.fromCharCode(units[i + 1], 3635);
153
+ i += 2;
154
+ } else if (c === 3635 && isThaiTone(units[i + 1])) {
155
+ out += String.fromCharCode(units[i + 1], 3635);
156
+ i++;
157
+ } else out += String.fromCharCode(c);
158
+ }
159
+ return out;
160
+ }
87
161
  var decompose = (s) => {
88
162
  try {
89
163
  return s.normalize("NFD");
@@ -92,16 +166,16 @@ var decompose = (s) => {
92
166
  }
93
167
  };
94
168
  function normalizeWords(text) {
95
- var _a;
169
+ var _a, _b;
96
170
  const source = decompose(String(text != null ? text : "").toLowerCase());
97
171
  let out = "";
98
172
  for (const raw of source) {
99
173
  const code = raw.charCodeAt(0);
100
- if (isCombiningMark(code)) continue;
101
- const ch = (_a = FOLD[raw]) != null ? _a : raw;
174
+ if (isCombiningMark(code) || isZeroWidth(code)) continue;
175
+ const ch = (_b = (_a = FOLD[raw]) != null ? _a : asciiDigit(code)) != null ? _b : raw;
102
176
  out += ch.length === 1 && !isWordChar(ch) ? " " : ch;
103
177
  }
104
- const cleaned = out.replace(/\s+/g, " ").trim();
178
+ const cleaned = orderMarks(out).replace(/\s+/g, " ").trim();
105
179
  return cleaned ? cleaned.split(" ") : [];
106
180
  }
107
181
  var STOP_WORDS = {
@@ -114,7 +188,9 @@ var STOP_WORDS = {
114
188
  de: ["wie", "was", "ich", "mein", "meine", "mir", "der", "die", "das", "ein", "eine", "und", "oder", "in", "mit", "fur", "von", "zu", "ist", "kann", "mochte", "wann", "wo", "warum", "nicht", "es", "den", "dem"],
115
189
  pt: ["como", "que", "eu", "meu", "minha", "o", "a", "os", "as", "um", "uma", "de", "do", "da", "em", "no", "na", "com", "para", "por", "e", "ou", "posso", "quero", "quando", "onde", "porque", "nao"],
116
190
  it: ["come", "che", "cosa", "io", "mio", "mia", "il", "lo", "la", "i", "gli", "le", "un", "una", "di", "del", "della", "in", "con", "per", "e", "o", "posso", "voglio", "quando", "dove", "perche", "non", "l"],
117
- tr: ["nasil", "ne", "ben", "benim", "bir", "ve", "veya", "ile", "icin", "bu", "su", "mi", "mu", "nerede", "ne zaman", "neden", "kac", "istiyorum", "var", "yok", "de", "da"]
191
+ tr: ["nasil", "ne", "ben", "benim", "bir", "ve", "veya", "ile", "icin", "bu", "su", "mi", "mu", "nerede", "ne zaman", "neden", "kac", "istiyorum", "var", "yok", "de", "da"],
192
+ // בתאית מילות קישור נמחקות רק כשהן מילה נפרדת (מוקפות ברווחים). רוב השאלות בלי רווחים, וההתאמה שם לפי תת-מחרוזת
193
+ th: ["\u0E09\u0E31\u0E19", "\u0E1C\u0E21", "\u0E14\u0E34\u0E09\u0E31\u0E19", "\u0E04\u0E38\u0E13", "\u0E04\u0E23\u0E31\u0E1A", "\u0E04\u0E48\u0E30", "\u0E04\u0E30", "\u0E19\u0E30", "\u0E44\u0E2B\u0E21", "\u0E21\u0E31\u0E49\u0E22", "\u0E2D\u0E30\u0E44\u0E23", "\u0E2D\u0E22\u0E48\u0E32\u0E07\u0E44\u0E23", "\u0E22\u0E31\u0E07\u0E44\u0E07", "\u0E17\u0E35\u0E48", "\u0E41\u0E25\u0E30", "\u0E2B\u0E23\u0E37\u0E2D", "\u0E02\u0E2D\u0E07", "\u0E44\u0E14\u0E49", "\u0E08\u0E30", "\u0E43\u0E19", "\u0E01\u0E31\u0E1A", "\u0E40\u0E1B\u0E47\u0E19", "\u0E21\u0E35", "\u0E44\u0E21\u0E48", "\u0E43\u0E2B\u0E49", "\u0E27\u0E48\u0E32", "\u0E2D\u0E22\u0E32\u0E01", "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23", "\u0E40\u0E21\u0E37\u0E48\u0E2D\u0E44\u0E2B\u0E23\u0E48", "\u0E17\u0E35\u0E48\u0E44\u0E2B\u0E19", "\u0E17\u0E33\u0E44\u0E21", "\u0E40\u0E17\u0E48\u0E32\u0E44\u0E2B\u0E23\u0E48"]
118
194
  };
119
195
  var NORMALIZED_STOP_WORDS = {};
120
196
  for (const locale of SUPPORTED_LOCALES) {
@@ -189,6 +265,8 @@ function matchQuestion(scripts, text, locale = "en") {
189
265
  const terms = Array.from(new Set(keyTerms(text, locale)));
190
266
  if (terms.length === 0) return { kind: "none", matches: [] };
191
267
  const terms_ = (values) => values.flatMap((v) => keyTerms(v, locale));
268
+ const wordTerms = terms.filter((t) => !isSpacelessScript(t));
269
+ const haystack = normalizeWords(text).join("");
192
270
  const candidates = [];
193
271
  for (const script of scripts) {
194
272
  if (script.kind === "opening") continue;
@@ -206,20 +284,25 @@ function matchQuestion(scripts, text, locale = "en") {
206
284
  const isRoot = step.id === content.rootStepId;
207
285
  const strong = terms_([...step.keywords, ...isRoot ? content.keywords : []]);
208
286
  const weak = terms_([...isRoot ? [content.name] : [], ...(_b = labelsTo.get(step.id)) != null ? _b : []]);
287
+ const typoPool = [...strong, ...weak].filter((w) => !isSpacelessScript(w));
209
288
  let score = 0;
210
289
  const reasons = [];
211
- for (const term of terms) {
290
+ for (const term of wordTerms) {
212
291
  if (strong.includes(term)) {
213
292
  score += 3;
214
293
  reasons.push({ kind: "keyword", term });
215
294
  } else if (weak.includes(term)) {
216
295
  score += 2;
217
296
  reasons.push({ kind: "title", term });
218
- } else if ([...strong, ...weak].some((w) => withinOneEdit(term, w))) {
297
+ } else if (typoPool.some((w) => withinOneEdit(term, w))) {
219
298
  score += 0.5;
220
299
  reasons.push({ kind: "typo", term });
221
300
  }
222
301
  }
302
+ for (const hit of substringHits(strong, weak, haystack)) {
303
+ score += hit.weight;
304
+ reasons.push({ kind: hit.weight === 3 ? "keyword" : "title", term: hit.term });
305
+ }
223
306
  if (score > 0) candidates.push({ scriptId: script.id, stepId: step.id, score, reasons });
224
307
  }
225
308
  }
@@ -229,6 +312,18 @@ function matchQuestion(scripts, text, locale = "en") {
229
312
  if (!second || top.score >= second.score * 2) return { kind: "single", matches: [top] };
230
313
  return { kind: "multi", matches: candidates.filter((c) => c.score >= top.score / 2).slice(0, 3) };
231
314
  }
315
+ function substringHits(strong, weak, haystack) {
316
+ const found = [];
317
+ const seen = /* @__PURE__ */ new Set();
318
+ for (const [list, weight] of [[strong, 3], [weak, 2]]) {
319
+ for (const term of list) {
320
+ if (seen.has(term) || !isSpacelessScript(term)) continue;
321
+ seen.add(term);
322
+ if (haystack.includes(term)) found.push({ term, weight });
323
+ }
324
+ }
325
+ return found.filter((a) => !found.some((b) => b.term !== a.term && b.weight >= a.weight && b.term.includes(a.term)));
326
+ }
232
327
 
233
328
  // i18n/en.json
234
329
  var en_default = {
@@ -640,8 +735,49 @@ var tr_default = {
640
735
  "form.backToChat": "Sohbete d\xF6n"
641
736
  };
642
737
 
738
+ // i18n/th.json
739
+ var th_default = {
740
+ "chat.askAnswered": "\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E19\u0E35\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E15\u0E2D\u0E1A\u0E04\u0E33\u0E16\u0E32\u0E21\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48",
741
+ "chat.yes": "\u0E43\u0E0A\u0E48 \u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13",
742
+ "chat.no": "\u0E44\u0E21\u0E48\u0E43\u0E0A\u0E48 \u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E04\u0E27\u0E32\u0E21\u0E0A\u0E48\u0E27\u0E22\u0E40\u0E2B\u0E25\u0E37\u0E2D",
743
+ "chat.thanks": "\u0E22\u0E34\u0E19\u0E14\u0E35\u0E17\u0E35\u0E48\u0E44\u0E14\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E40\u0E2B\u0E25\u0E37\u0E2D",
744
+ "chat.newQuestion": "\u0E16\u0E32\u0E21\u0E04\u0E33\u0E16\u0E32\u0E21\u0E43\u0E2B\u0E21\u0E48",
745
+ "chat.back": "\u0E22\u0E49\u0E2D\u0E19\u0E01\u0E25\u0E31\u0E1A",
746
+ "chat.home": "\u0E40\u0E21\u0E19\u0E39\u0E2B\u0E25\u0E31\u0E01",
747
+ "chat.didYouMean": "\u0E04\u0E38\u0E13\u0E2B\u0E21\u0E32\u0E22\u0E16\u0E36\u0E07\u0E02\u0E49\u0E2D\u0E43\u0E14",
748
+ "chat.notFound": "\u0E02\u0E2D\u0E2D\u0E20\u0E31\u0E22 \u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E04\u0E33\u0E16\u0E32\u0E21\u0E19\u0E35\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2B\u0E31\u0E27\u0E02\u0E49\u0E2D\u0E08\u0E32\u0E01\u0E40\u0E21\u0E19\u0E39 \u0E2B\u0E23\u0E37\u0E2D\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E17\u0E35\u0E21\u0E07\u0E32\u0E19\u0E02\u0E2D\u0E07\u0E40\u0E23\u0E32",
749
+ "chat.notFoundTwice": "\u0E02\u0E2D\u0E2D\u0E20\u0E31\u0E22 \u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E04\u0E33\u0E15\u0E2D\u0E1A \u0E17\u0E35\u0E21\u0E07\u0E32\u0E19\u0E02\u0E2D\u0E07\u0E40\u0E23\u0E32\u0E22\u0E34\u0E19\u0E14\u0E35\u0E0A\u0E48\u0E27\u0E22\u0E40\u0E2B\u0E25\u0E37\u0E2D \u0E41\u0E25\u0E30\u0E44\u0E14\u0E49\u0E43\u0E2A\u0E48\u0E04\u0E33\u0E16\u0E32\u0E21\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E44\u0E27\u0E49\u0E43\u0E19\u0E41\u0E1A\u0E1A\u0E1F\u0E2D\u0E23\u0E4C\u0E21\u0E41\u0E25\u0E49\u0E27",
750
+ "chat.contactChip": "\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E17\u0E35\u0E21\u0E07\u0E32\u0E19",
751
+ "chat.openingContact": "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E40\u0E1B\u0E34\u0E14\u0E41\u0E1A\u0E1A\u0E1F\u0E2D\u0E23\u0E4C\u0E21\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D \u0E1A\u0E17\u0E2A\u0E19\u0E17\u0E19\u0E32\u0E19\u0E35\u0E49\u0E08\u0E30\u0E41\u0E19\u0E1A\u0E44\u0E1B\u0E01\u0E31\u0E1A\u0E04\u0E33\u0E02\u0E2D\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13",
752
+ "chat.chooseTopic": "\u0E21\u0E35\u0E2D\u0E30\u0E44\u0E23\u0E43\u0E2B\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E44\u0E2B\u0E21",
753
+ "chat.aiLabel": "\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E08\u0E32\u0E01 AI",
754
+ "widget.open": "\u0E40\u0E1B\u0E34\u0E14\u0E41\u0E0A\u0E15",
755
+ "widget.close": "\u0E1B\u0E34\u0E14\u0E41\u0E0A\u0E15",
756
+ "widget.placeholder": "\u0E1E\u0E34\u0E21\u0E1E\u0E4C\u0E04\u0E33\u0E16\u0E32\u0E21\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13...",
757
+ "widget.send": "\u0E2A\u0E48\u0E07",
758
+ "widget.poweredBy": "\u0E02\u0E31\u0E1A\u0E40\u0E04\u0E25\u0E37\u0E48\u0E2D\u0E19\u0E42\u0E14\u0E22 Kavchat",
759
+ "widget.offline": "\u0E04\u0E38\u0E13\u0E2D\u0E2D\u0E1F\u0E44\u0E25\u0E19\u0E4C\u0E2D\u0E22\u0E39\u0E48 \u0E01\u0E33\u0E25\u0E31\u0E07\u0E41\u0E2A\u0E14\u0E07\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E17\u0E35\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E44\u0E27\u0E49",
760
+ "widget.loading": "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14...",
761
+ "widget.unavailable": "\u0E1C\u0E39\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E44\u0E21\u0E48\u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E43\u0E2B\u0E49\u0E1A\u0E23\u0E34\u0E01\u0E32\u0E23\u0E43\u0E19\u0E02\u0E13\u0E30\u0E19\u0E35\u0E49",
762
+ "form.title": "\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E40\u0E23\u0E32",
763
+ "form.name": "\u0E0A\u0E37\u0E48\u0E2D",
764
+ "form.email": "\u0E2D\u0E35\u0E40\u0E21\u0E25",
765
+ "form.phone": "\u0E42\u0E17\u0E23\u0E28\u0E31\u0E1E\u0E17\u0E4C",
766
+ "form.topic": "\u0E2B\u0E31\u0E27\u0E02\u0E49\u0E2D",
767
+ "form.message": "\u0E02\u0E49\u0E2D\u0E04\u0E27\u0E32\u0E21",
768
+ "form.submit": "\u0E2A\u0E48\u0E07",
769
+ "form.cancel": "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01",
770
+ "form.sending": "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2A\u0E48\u0E07...",
771
+ "form.sent": "\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13 \u0E40\u0E23\u0E32\u0E44\u0E14\u0E49\u0E23\u0E31\u0E1A\u0E04\u0E33\u0E02\u0E2D\u0E2B\u0E21\u0E32\u0E22\u0E40\u0E25\u0E02 {number} \u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E41\u0E25\u0E49\u0E27 \u0E41\u0E25\u0E30\u0E08\u0E30\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E01\u0E25\u0E31\u0E1A\u0E42\u0E14\u0E22\u0E40\u0E23\u0E47\u0E27",
772
+ "form.error": "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07",
773
+ "form.required": "\u0E08\u0E33\u0E40\u0E1B\u0E47\u0E19\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E23\u0E2D\u0E01",
774
+ "form.contactRequired": "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E23\u0E30\u0E1A\u0E38\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E2B\u0E23\u0E37\u0E2D\u0E2B\u0E21\u0E32\u0E22\u0E40\u0E25\u0E02\u0E42\u0E17\u0E23\u0E28\u0E31\u0E1E\u0E17\u0E4C \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E43\u0E2B\u0E49\u0E40\u0E23\u0E32\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E01\u0E25\u0E31\u0E1A\u0E44\u0E14\u0E49",
775
+ "form.emailInvalid": "\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07",
776
+ "form.backToChat": "\u0E01\u0E25\u0E31\u0E1A\u0E44\u0E1B\u0E17\u0E35\u0E48\u0E41\u0E0A\u0E15"
777
+ };
778
+
643
779
  // src/i18n.ts
644
- var UI_TEXTS = { en: en_default, he: he_default, ar: ar_default, ru: ru_default, es: es_default, fr: fr_default, de: de_default, pt: pt_default, it: it_default, tr: tr_default };
780
+ var UI_TEXTS = { en: en_default, he: he_default, ar: ar_default, ru: ru_default, es: es_default, fr: fr_default, de: de_default, pt: pt_default, it: it_default, tr: tr_default, th: th_default };
645
781
  var RTL_LOCALES = ["he", "ar"];
646
782
  var isRtl = (locale) => RTL_LOCALES.includes(baseLocale(locale));
647
783
  var baseLocale = (locale) => String(locale || "en").toLowerCase().split(/[-_]/)[0].replace(/^iw$/, "he");
@@ -925,6 +1061,7 @@ function eventsPayload(state, sessionId, releaseVersion, startedAt) {
925
1061
  format,
926
1062
  groupKey,
927
1063
  isRtl,
1064
+ isSpacelessScript,
928
1065
  keyTerms,
929
1066
  matchQuestion,
930
1067
  normalizeWords,
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/normalize.ts
2
- var SUPPORTED_LOCALES = ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr"];
2
+ var SUPPORTED_LOCALES = ["he", "en", "ar", "ru", "es", "fr", "de", "pt", "it", "tr", "th"];
3
3
  var FOLD = {
4
4
  // עברית: אותיות סופיות
5
5
  \u05DA: "\u05DB",
@@ -25,6 +25,13 @@ var FOLD = {
25
25
  function isCombiningMark(code) {
26
26
  return code >= 768 && code <= 879 || code >= 1425 && code <= 1479 || code >= 1552 && code <= 1562 || code >= 1611 && code <= 1631 || code === 1648 || code >= 1750 && code <= 1773;
27
27
  }
28
+ function isSpacelessLetter(code) {
29
+ return code >= 3585 && code <= 3630 || code >= 3632 && code <= 3642 || code >= 3648 && code <= 3653 || code >= 3655 && code <= 3662 || code >= 3713 && code <= 3758 || code >= 3760 && code <= 3781 || code >= 3784 && code <= 3790 || code >= 3804 && code <= 3807 || code >= 4096 && code <= 4169 || code >= 4176 && code <= 4253 || code >= 6016 && code <= 6099 || code === 6108 || code === 6109 || code === 12293 || code >= 12353 && code <= 12438 || code >= 12441 && code <= 12442 || code >= 12445 && code <= 12447 || code >= 12449 && code <= 12538 || code >= 12540 && code <= 12543 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959;
30
+ }
31
+ function isSpacelessScript(word) {
32
+ for (let i = 0; i < word.length; i++) if (isSpacelessLetter(word.charCodeAt(i))) return true;
33
+ return false;
34
+ }
28
35
  function isWordChar(ch) {
29
36
  const code = ch.charCodeAt(0);
30
37
  if (code >= 48 && code <= 57) return true;
@@ -32,9 +39,75 @@ function isWordChar(ch) {
32
39
  if (code >= 1488 && code <= 1514) return true;
33
40
  if (code >= 1568 && code <= 1610) return true;
34
41
  if (code >= 1649 && code <= 1747) return true;
35
- if (code >= 19968 && code <= 40959) return true;
42
+ if (isSpacelessLetter(code)) return true;
36
43
  return ch.toLowerCase() !== ch.toUpperCase();
37
44
  }
45
+ var isZeroWidth = (code) => code >= 8203 && code <= 8205 || code === 8288 || code === 65279;
46
+ var DIGIT_ZEROS = [3664, 3792, 4160, 6112];
47
+ function asciiDigit(code) {
48
+ for (const zero of DIGIT_ZEROS) if (code >= zero && code <= zero + 9) return String.fromCharCode(48 + code - zero);
49
+ return void 0;
50
+ }
51
+ var MARK_ORDER = {
52
+ 3640: 103,
53
+ 3641: 103,
54
+ 3642: 9,
55
+ 3656: 107,
56
+ 3657: 107,
57
+ 3658: 107,
58
+ 3659: 107,
59
+ 3768: 118,
60
+ 3769: 118,
61
+ 3770: 9,
62
+ 3784: 122,
63
+ 3785: 122,
64
+ 3786: 122,
65
+ 3787: 122,
66
+ 4151: 7,
67
+ 4153: 9,
68
+ 4154: 9,
69
+ 4237: 220,
70
+ 6098: 9,
71
+ 6109: 230,
72
+ 12441: 8,
73
+ 12442: 8
74
+ };
75
+ var isThaiTone = (code) => code >= 3656 && code <= 3659;
76
+ function orderMarks(text) {
77
+ if (!/[\u0e33\u0e38-\u0e3a\u0e48-\u0e4b\u0e4d\u0eb8-\u0eba\u0ec8-\u0ecb\u1037\u1039\u103a\u108d\u17d2\u17dd\u3099\u309a]/.test(text)) return text;
78
+ const units = [];
79
+ for (let i = 0; i < text.length; i++) units.push(text.charCodeAt(i));
80
+ for (let i = 0; i < units.length; i++) {
81
+ if (!MARK_ORDER[units[i]]) continue;
82
+ let end = i;
83
+ while (end < units.length && MARK_ORDER[units[end]]) end++;
84
+ for (let a = i + 1; a < end; a++) {
85
+ const v = units[a];
86
+ let b = a - 1;
87
+ while (b >= i && MARK_ORDER[units[b]] > MARK_ORDER[v]) {
88
+ units[b + 1] = units[b];
89
+ b--;
90
+ }
91
+ units[b + 1] = v;
92
+ }
93
+ i = end - 1;
94
+ }
95
+ let out = "";
96
+ for (let i = 0; i < units.length; i++) {
97
+ const c = units[i];
98
+ if (c === 3661 && units[i + 1] === 3634) {
99
+ out += "\u0E33";
100
+ i++;
101
+ } else if (c === 3661 && isThaiTone(units[i + 1]) && units[i + 2] === 3634) {
102
+ out += String.fromCharCode(units[i + 1], 3635);
103
+ i += 2;
104
+ } else if (c === 3635 && isThaiTone(units[i + 1])) {
105
+ out += String.fromCharCode(units[i + 1], 3635);
106
+ i++;
107
+ } else out += String.fromCharCode(c);
108
+ }
109
+ return out;
110
+ }
38
111
  var decompose = (s) => {
39
112
  try {
40
113
  return s.normalize("NFD");
@@ -43,16 +116,16 @@ var decompose = (s) => {
43
116
  }
44
117
  };
45
118
  function normalizeWords(text) {
46
- var _a;
119
+ var _a, _b;
47
120
  const source = decompose(String(text != null ? text : "").toLowerCase());
48
121
  let out = "";
49
122
  for (const raw of source) {
50
123
  const code = raw.charCodeAt(0);
51
- if (isCombiningMark(code)) continue;
52
- const ch = (_a = FOLD[raw]) != null ? _a : raw;
124
+ if (isCombiningMark(code) || isZeroWidth(code)) continue;
125
+ const ch = (_b = (_a = FOLD[raw]) != null ? _a : asciiDigit(code)) != null ? _b : raw;
53
126
  out += ch.length === 1 && !isWordChar(ch) ? " " : ch;
54
127
  }
55
- const cleaned = out.replace(/\s+/g, " ").trim();
128
+ const cleaned = orderMarks(out).replace(/\s+/g, " ").trim();
56
129
  return cleaned ? cleaned.split(" ") : [];
57
130
  }
58
131
  var STOP_WORDS = {
@@ -65,7 +138,9 @@ var STOP_WORDS = {
65
138
  de: ["wie", "was", "ich", "mein", "meine", "mir", "der", "die", "das", "ein", "eine", "und", "oder", "in", "mit", "fur", "von", "zu", "ist", "kann", "mochte", "wann", "wo", "warum", "nicht", "es", "den", "dem"],
66
139
  pt: ["como", "que", "eu", "meu", "minha", "o", "a", "os", "as", "um", "uma", "de", "do", "da", "em", "no", "na", "com", "para", "por", "e", "ou", "posso", "quero", "quando", "onde", "porque", "nao"],
67
140
  it: ["come", "che", "cosa", "io", "mio", "mia", "il", "lo", "la", "i", "gli", "le", "un", "una", "di", "del", "della", "in", "con", "per", "e", "o", "posso", "voglio", "quando", "dove", "perche", "non", "l"],
68
- tr: ["nasil", "ne", "ben", "benim", "bir", "ve", "veya", "ile", "icin", "bu", "su", "mi", "mu", "nerede", "ne zaman", "neden", "kac", "istiyorum", "var", "yok", "de", "da"]
141
+ tr: ["nasil", "ne", "ben", "benim", "bir", "ve", "veya", "ile", "icin", "bu", "su", "mi", "mu", "nerede", "ne zaman", "neden", "kac", "istiyorum", "var", "yok", "de", "da"],
142
+ // בתאית מילות קישור נמחקות רק כשהן מילה נפרדת (מוקפות ברווחים). רוב השאלות בלי רווחים, וההתאמה שם לפי תת-מחרוזת
143
+ th: ["\u0E09\u0E31\u0E19", "\u0E1C\u0E21", "\u0E14\u0E34\u0E09\u0E31\u0E19", "\u0E04\u0E38\u0E13", "\u0E04\u0E23\u0E31\u0E1A", "\u0E04\u0E48\u0E30", "\u0E04\u0E30", "\u0E19\u0E30", "\u0E44\u0E2B\u0E21", "\u0E21\u0E31\u0E49\u0E22", "\u0E2D\u0E30\u0E44\u0E23", "\u0E2D\u0E22\u0E48\u0E32\u0E07\u0E44\u0E23", "\u0E22\u0E31\u0E07\u0E44\u0E07", "\u0E17\u0E35\u0E48", "\u0E41\u0E25\u0E30", "\u0E2B\u0E23\u0E37\u0E2D", "\u0E02\u0E2D\u0E07", "\u0E44\u0E14\u0E49", "\u0E08\u0E30", "\u0E43\u0E19", "\u0E01\u0E31\u0E1A", "\u0E40\u0E1B\u0E47\u0E19", "\u0E21\u0E35", "\u0E44\u0E21\u0E48", "\u0E43\u0E2B\u0E49", "\u0E27\u0E48\u0E32", "\u0E2D\u0E22\u0E32\u0E01", "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23", "\u0E40\u0E21\u0E37\u0E48\u0E2D\u0E44\u0E2B\u0E23\u0E48", "\u0E17\u0E35\u0E48\u0E44\u0E2B\u0E19", "\u0E17\u0E33\u0E44\u0E21", "\u0E40\u0E17\u0E48\u0E32\u0E44\u0E2B\u0E23\u0E48"]
69
144
  };
70
145
  var NORMALIZED_STOP_WORDS = {};
71
146
  for (const locale of SUPPORTED_LOCALES) {
@@ -140,6 +215,8 @@ function matchQuestion(scripts, text, locale = "en") {
140
215
  const terms = Array.from(new Set(keyTerms(text, locale)));
141
216
  if (terms.length === 0) return { kind: "none", matches: [] };
142
217
  const terms_ = (values) => values.flatMap((v) => keyTerms(v, locale));
218
+ const wordTerms = terms.filter((t) => !isSpacelessScript(t));
219
+ const haystack = normalizeWords(text).join("");
143
220
  const candidates = [];
144
221
  for (const script of scripts) {
145
222
  if (script.kind === "opening") continue;
@@ -157,20 +234,25 @@ function matchQuestion(scripts, text, locale = "en") {
157
234
  const isRoot = step.id === content.rootStepId;
158
235
  const strong = terms_([...step.keywords, ...isRoot ? content.keywords : []]);
159
236
  const weak = terms_([...isRoot ? [content.name] : [], ...(_b = labelsTo.get(step.id)) != null ? _b : []]);
237
+ const typoPool = [...strong, ...weak].filter((w) => !isSpacelessScript(w));
160
238
  let score = 0;
161
239
  const reasons = [];
162
- for (const term of terms) {
240
+ for (const term of wordTerms) {
163
241
  if (strong.includes(term)) {
164
242
  score += 3;
165
243
  reasons.push({ kind: "keyword", term });
166
244
  } else if (weak.includes(term)) {
167
245
  score += 2;
168
246
  reasons.push({ kind: "title", term });
169
- } else if ([...strong, ...weak].some((w) => withinOneEdit(term, w))) {
247
+ } else if (typoPool.some((w) => withinOneEdit(term, w))) {
170
248
  score += 0.5;
171
249
  reasons.push({ kind: "typo", term });
172
250
  }
173
251
  }
252
+ for (const hit of substringHits(strong, weak, haystack)) {
253
+ score += hit.weight;
254
+ reasons.push({ kind: hit.weight === 3 ? "keyword" : "title", term: hit.term });
255
+ }
174
256
  if (score > 0) candidates.push({ scriptId: script.id, stepId: step.id, score, reasons });
175
257
  }
176
258
  }
@@ -180,6 +262,18 @@ function matchQuestion(scripts, text, locale = "en") {
180
262
  if (!second || top.score >= second.score * 2) return { kind: "single", matches: [top] };
181
263
  return { kind: "multi", matches: candidates.filter((c) => c.score >= top.score / 2).slice(0, 3) };
182
264
  }
265
+ function substringHits(strong, weak, haystack) {
266
+ const found = [];
267
+ const seen = /* @__PURE__ */ new Set();
268
+ for (const [list, weight] of [[strong, 3], [weak, 2]]) {
269
+ for (const term of list) {
270
+ if (seen.has(term) || !isSpacelessScript(term)) continue;
271
+ seen.add(term);
272
+ if (haystack.includes(term)) found.push({ term, weight });
273
+ }
274
+ }
275
+ return found.filter((a) => !found.some((b) => b.term !== a.term && b.weight >= a.weight && b.term.includes(a.term)));
276
+ }
183
277
 
184
278
  // i18n/en.json
185
279
  var en_default = {
@@ -591,8 +685,49 @@ var tr_default = {
591
685
  "form.backToChat": "Sohbete d\xF6n"
592
686
  };
593
687
 
688
+ // i18n/th.json
689
+ var th_default = {
690
+ "chat.askAnswered": "\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E19\u0E35\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E15\u0E2D\u0E1A\u0E04\u0E33\u0E16\u0E32\u0E21\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48",
691
+ "chat.yes": "\u0E43\u0E0A\u0E48 \u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13",
692
+ "chat.no": "\u0E44\u0E21\u0E48\u0E43\u0E0A\u0E48 \u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E04\u0E27\u0E32\u0E21\u0E0A\u0E48\u0E27\u0E22\u0E40\u0E2B\u0E25\u0E37\u0E2D",
693
+ "chat.thanks": "\u0E22\u0E34\u0E19\u0E14\u0E35\u0E17\u0E35\u0E48\u0E44\u0E14\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E40\u0E2B\u0E25\u0E37\u0E2D",
694
+ "chat.newQuestion": "\u0E16\u0E32\u0E21\u0E04\u0E33\u0E16\u0E32\u0E21\u0E43\u0E2B\u0E21\u0E48",
695
+ "chat.back": "\u0E22\u0E49\u0E2D\u0E19\u0E01\u0E25\u0E31\u0E1A",
696
+ "chat.home": "\u0E40\u0E21\u0E19\u0E39\u0E2B\u0E25\u0E31\u0E01",
697
+ "chat.didYouMean": "\u0E04\u0E38\u0E13\u0E2B\u0E21\u0E32\u0E22\u0E16\u0E36\u0E07\u0E02\u0E49\u0E2D\u0E43\u0E14",
698
+ "chat.notFound": "\u0E02\u0E2D\u0E2D\u0E20\u0E31\u0E22 \u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E04\u0E33\u0E16\u0E32\u0E21\u0E19\u0E35\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2B\u0E31\u0E27\u0E02\u0E49\u0E2D\u0E08\u0E32\u0E01\u0E40\u0E21\u0E19\u0E39 \u0E2B\u0E23\u0E37\u0E2D\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E17\u0E35\u0E21\u0E07\u0E32\u0E19\u0E02\u0E2D\u0E07\u0E40\u0E23\u0E32",
699
+ "chat.notFoundTwice": "\u0E02\u0E2D\u0E2D\u0E20\u0E31\u0E22 \u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E04\u0E33\u0E15\u0E2D\u0E1A \u0E17\u0E35\u0E21\u0E07\u0E32\u0E19\u0E02\u0E2D\u0E07\u0E40\u0E23\u0E32\u0E22\u0E34\u0E19\u0E14\u0E35\u0E0A\u0E48\u0E27\u0E22\u0E40\u0E2B\u0E25\u0E37\u0E2D \u0E41\u0E25\u0E30\u0E44\u0E14\u0E49\u0E43\u0E2A\u0E48\u0E04\u0E33\u0E16\u0E32\u0E21\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E44\u0E27\u0E49\u0E43\u0E19\u0E41\u0E1A\u0E1A\u0E1F\u0E2D\u0E23\u0E4C\u0E21\u0E41\u0E25\u0E49\u0E27",
700
+ "chat.contactChip": "\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E17\u0E35\u0E21\u0E07\u0E32\u0E19",
701
+ "chat.openingContact": "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E40\u0E1B\u0E34\u0E14\u0E41\u0E1A\u0E1A\u0E1F\u0E2D\u0E23\u0E4C\u0E21\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D \u0E1A\u0E17\u0E2A\u0E19\u0E17\u0E19\u0E32\u0E19\u0E35\u0E49\u0E08\u0E30\u0E41\u0E19\u0E1A\u0E44\u0E1B\u0E01\u0E31\u0E1A\u0E04\u0E33\u0E02\u0E2D\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13",
702
+ "chat.chooseTopic": "\u0E21\u0E35\u0E2D\u0E30\u0E44\u0E23\u0E43\u0E2B\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E44\u0E2B\u0E21",
703
+ "chat.aiLabel": "\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E08\u0E32\u0E01 AI",
704
+ "widget.open": "\u0E40\u0E1B\u0E34\u0E14\u0E41\u0E0A\u0E15",
705
+ "widget.close": "\u0E1B\u0E34\u0E14\u0E41\u0E0A\u0E15",
706
+ "widget.placeholder": "\u0E1E\u0E34\u0E21\u0E1E\u0E4C\u0E04\u0E33\u0E16\u0E32\u0E21\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13...",
707
+ "widget.send": "\u0E2A\u0E48\u0E07",
708
+ "widget.poweredBy": "\u0E02\u0E31\u0E1A\u0E40\u0E04\u0E25\u0E37\u0E48\u0E2D\u0E19\u0E42\u0E14\u0E22 Kavchat",
709
+ "widget.offline": "\u0E04\u0E38\u0E13\u0E2D\u0E2D\u0E1F\u0E44\u0E25\u0E19\u0E4C\u0E2D\u0E22\u0E39\u0E48 \u0E01\u0E33\u0E25\u0E31\u0E07\u0E41\u0E2A\u0E14\u0E07\u0E04\u0E33\u0E15\u0E2D\u0E1A\u0E17\u0E35\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E44\u0E27\u0E49",
710
+ "widget.loading": "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14...",
711
+ "widget.unavailable": "\u0E1C\u0E39\u0E49\u0E0A\u0E48\u0E27\u0E22\u0E44\u0E21\u0E48\u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E43\u0E2B\u0E49\u0E1A\u0E23\u0E34\u0E01\u0E32\u0E23\u0E43\u0E19\u0E02\u0E13\u0E30\u0E19\u0E35\u0E49",
712
+ "form.title": "\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E40\u0E23\u0E32",
713
+ "form.name": "\u0E0A\u0E37\u0E48\u0E2D",
714
+ "form.email": "\u0E2D\u0E35\u0E40\u0E21\u0E25",
715
+ "form.phone": "\u0E42\u0E17\u0E23\u0E28\u0E31\u0E1E\u0E17\u0E4C",
716
+ "form.topic": "\u0E2B\u0E31\u0E27\u0E02\u0E49\u0E2D",
717
+ "form.message": "\u0E02\u0E49\u0E2D\u0E04\u0E27\u0E32\u0E21",
718
+ "form.submit": "\u0E2A\u0E48\u0E07",
719
+ "form.cancel": "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01",
720
+ "form.sending": "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2A\u0E48\u0E07...",
721
+ "form.sent": "\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13 \u0E40\u0E23\u0E32\u0E44\u0E14\u0E49\u0E23\u0E31\u0E1A\u0E04\u0E33\u0E02\u0E2D\u0E2B\u0E21\u0E32\u0E22\u0E40\u0E25\u0E02 {number} \u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E41\u0E25\u0E49\u0E27 \u0E41\u0E25\u0E30\u0E08\u0E30\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E01\u0E25\u0E31\u0E1A\u0E42\u0E14\u0E22\u0E40\u0E23\u0E47\u0E27",
722
+ "form.error": "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07",
723
+ "form.required": "\u0E08\u0E33\u0E40\u0E1B\u0E47\u0E19\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E23\u0E2D\u0E01",
724
+ "form.contactRequired": "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E23\u0E30\u0E1A\u0E38\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E2B\u0E23\u0E37\u0E2D\u0E2B\u0E21\u0E32\u0E22\u0E40\u0E25\u0E02\u0E42\u0E17\u0E23\u0E28\u0E31\u0E1E\u0E17\u0E4C \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E43\u0E2B\u0E49\u0E40\u0E23\u0E32\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E01\u0E25\u0E31\u0E1A\u0E44\u0E14\u0E49",
725
+ "form.emailInvalid": "\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07",
726
+ "form.backToChat": "\u0E01\u0E25\u0E31\u0E1A\u0E44\u0E1B\u0E17\u0E35\u0E48\u0E41\u0E0A\u0E15"
727
+ };
728
+
594
729
  // src/i18n.ts
595
- var UI_TEXTS = { en: en_default, he: he_default, ar: ar_default, ru: ru_default, es: es_default, fr: fr_default, de: de_default, pt: pt_default, it: it_default, tr: tr_default };
730
+ var UI_TEXTS = { en: en_default, he: he_default, ar: ar_default, ru: ru_default, es: es_default, fr: fr_default, de: de_default, pt: pt_default, it: it_default, tr: tr_default, th: th_default };
596
731
  var RTL_LOCALES = ["he", "ar"];
597
732
  var isRtl = (locale) => RTL_LOCALES.includes(baseLocale(locale));
598
733
  var baseLocale = (locale) => String(locale || "en").toLowerCase().split(/[-_]/)[0].replace(/^iw$/, "he");
@@ -875,6 +1010,7 @@ export {
875
1010
  format,
876
1011
  groupKey,
877
1012
  isRtl,
1013
+ isSpacelessScript,
878
1014
  keyTerms,
879
1015
  matchQuestion,
880
1016
  normalizeWords,
package/i18n/th.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "chat.askAnswered": "คำตอบนี้ช่วยตอบคำถามของคุณหรือไม่",
3
+ "chat.yes": "ใช่ ขอบคุณ",
4
+ "chat.no": "ไม่ใช่ ต้องการความช่วยเหลือ",
5
+ "chat.thanks": "ยินดีที่ได้ช่วยเหลือ",
6
+ "chat.newQuestion": "ถามคำถามใหม่",
7
+ "chat.back": "ย้อนกลับ",
8
+ "chat.home": "เมนูหลัก",
9
+ "chat.didYouMean": "คุณหมายถึงข้อใด",
10
+ "chat.notFound": "ขออภัย ไม่พบคำตอบสำหรับคำถามนี้ กรุณาเลือกหัวข้อจากเมนู หรือติดต่อทีมงานของเรา",
11
+ "chat.notFoundTwice": "ขออภัย ไม่พบคำตอบ ทีมงานของเรายินดีช่วยเหลือ และได้ใส่คำถามของคุณไว้ในแบบฟอร์มแล้ว",
12
+ "chat.contactChip": "ติดต่อทีมงาน",
13
+ "chat.openingContact": "กำลังเปิดแบบฟอร์มติดต่อ บทสนทนานี้จะแนบไปกับคำขอของคุณ",
14
+ "chat.chooseTopic": "มีอะไรให้ช่วยไหม",
15
+ "chat.aiLabel": "คำตอบจาก AI",
16
+ "widget.open": "เปิดแชต",
17
+ "widget.close": "ปิดแชต",
18
+ "widget.placeholder": "พิมพ์คำถามของคุณ...",
19
+ "widget.send": "ส่ง",
20
+ "widget.poweredBy": "ขับเคลื่อนโดย Kavchat",
21
+ "widget.offline": "คุณออฟไลน์อยู่ กำลังแสดงคำตอบที่บันทึกไว้",
22
+ "widget.loading": "กำลังโหลด...",
23
+ "widget.unavailable": "ผู้ช่วยไม่พร้อมให้บริการในขณะนี้",
24
+ "form.title": "ติดต่อเรา",
25
+ "form.name": "ชื่อ",
26
+ "form.email": "อีเมล",
27
+ "form.phone": "โทรศัพท์",
28
+ "form.topic": "หัวข้อ",
29
+ "form.message": "ข้อความ",
30
+ "form.submit": "ส่ง",
31
+ "form.cancel": "ยกเลิก",
32
+ "form.sending": "กำลังส่ง...",
33
+ "form.sent": "ขอบคุณ เราได้รับคำขอหมายเลข {number} ของคุณแล้ว และจะติดต่อกลับโดยเร็ว",
34
+ "form.error": "เกิดข้อผิดพลาด กรุณาลองอีกครั้ง",
35
+ "form.required": "จำเป็นต้องกรอก",
36
+ "form.contactRequired": "กรุณาระบุอีเมลหรือหมายเลขโทรศัพท์ เพื่อให้เราติดต่อกลับได้",
37
+ "form.emailInvalid": "อีเมลไม่ถูกต้อง",
38
+ "form.backToChat": "กลับไปที่แชต"
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kavchat/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Kavchat conversation engine: text normalization, question matching and the chat state machine. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",