kiku-agent 1.0.11 → 1.2.0

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.
Files changed (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -0,0 +1,1089 @@
1
+
2
+
3
+ import { eventBus, Events } from './eventBus.js';
4
+
5
+ // Languages supported by the clarifying-question system. Add a new code here
6
+ // and fill its entries in the per-language `questions` / `options` maps below
7
+ // to extend coverage.
8
+ export const SUPPORTED_LANGUAGES = ['en', 'th', 'zh', 'ja', 'ko'];
9
+
10
+ const LANGUAGE_ALIASES = {
11
+ jp: 'ja',
12
+ kr: 'ko',
13
+ cn: 'zh',
14
+ 'zh-cn': 'zh',
15
+ 'zh-hans': 'zh',
16
+ 'zh-hant': 'zh',
17
+ 'zh-tw': 'zh',
18
+ 'zh-hk': 'zh',
19
+ 'en-us': 'en',
20
+ 'en-gb': 'en',
21
+ 'th-th': 'th',
22
+ 'ja-jp': 'ja',
23
+ 'ko-kr': 'ko',
24
+ };
25
+
26
+ // Normalize any user/system supplied language tag (e.g. "JP", "zh-TW") to a
27
+ // supported code; unknown tags fall back to English.
28
+ export function normalizeLang(lang, fallback = 'en') {
29
+ if (!lang || typeof lang !== 'string') return fallback;
30
+ const key = lang.trim().toLowerCase().replace('_', '-');
31
+ if (SUPPORTED_LANGUAGES.includes(key)) return key;
32
+ if (LANGUAGE_ALIASES[key]) return LANGUAGE_ALIASES[key];
33
+ const short = key.split('-')[0];
34
+ if (SUPPORTED_LANGUAGES.includes(short)) return short;
35
+ return fallback;
36
+ }
37
+
38
+ const AMBIGUITY_PATTERNS = [
39
+ {
40
+ category: 'timeframe',
41
+ weight: 0.7,
42
+ detection: /\b(recent|latest|new|old|upcoming|past|this year|last year|this month|earliest|newest)\b|(ล่าสุด|ใหม่|เก่า|ย้อนหลัง|最新|最近|昔|以前|최신|최근|예전|최근작)/i,
43
+ questions: {
44
+ en: 'Could you specify the timeframe you\'re interested in? For example: 2024-2026, 2020-2023, or all time?',
45
+ th: 'กรุณาระบุช่วงเวลาที่สนใจ เช่น ปี 2024-2026, ปี 2020-2023 หรือทั้งหมด',
46
+ zh: '请问您感兴趣的时间范围是?例如:2024-2026年、2020-2023年、或全部时期?',
47
+ ja: 'ご興味のある期間を教えていただけますか?例:2024-2026年、2020-2023年、または全期間',
48
+ ko: '어떤 기간에 관심이 있으신가요? 예: 2024-2026년, 2020-2023년, 또는 전체 기간'
49
+ },
50
+ options: {
51
+ en: ['2024-2026 (latest)', '2020-2023', 'All time'],
52
+ th: ['2024-2026 (ล่าสุด)', '2020-2023', 'All time (ทั้งหมด)'],
53
+ zh: ['2024-2026 年(最新)', '2020-2023 年', '全部时期'],
54
+ ja: ['2024-2026年(最新)', '2020-2023年', '全期間'],
55
+ ko: ['2024-2026년 (최신)', '2020-2023년', '전체 기간']
56
+ }
57
+ },
58
+ {
59
+ category: 'scope',
60
+ weight: 0.6,
61
+ detection: /\b(all|every|list|show|tell me about|what about|how about|comprehensive|complete)\b|(ทั้งหมด|ทุกอย่าง|ภาพรวม|全部|一覧|まとめ|모든|전체|목록)/i,
62
+ questions: {
63
+ en: 'Would you like a comprehensive overview or a focused summary on a specific aspect?',
64
+ th: 'ต้องการภาพรวมที่ครอบคลุม หรือต้องการเน้นเฉพาะด้านใดด้านหนึ่ง?',
65
+ zh: '您想要全面的概览还是针对特定方面的重点介绍?',
66
+ ja: '包括的な概覧をご希望ですか、それとも特定の側面に絞った要約をご希望ですか?',
67
+ ko: '전체적인 개요를 원하시나요, 아니면 특정 측면에 집중한 요약을 원하시나요?'
68
+ },
69
+ options: {
70
+ en: ['Comprehensive overview', 'Focused summary', 'Key highlights only'],
71
+ th: ['ภาพรวมครอบคลุม', 'สรุปเฉพาะประเด็น', 'เฉพาะไฮไลต์สำคัญ'],
72
+ zh: ['全面概览', '聚焦摘要', '仅重点亮点'],
73
+ ja: ['包括的な概要', '要点を絞った要約', 'ハイライトのみ'],
74
+ ko: ['전체 개요', '핵심 요약', '주요 하이라이트만']
75
+ }
76
+ },
77
+ {
78
+ category: 'depth',
79
+ weight: 0.5,
80
+ detection: /\b(detailed|comprehensive|in-depth|brief|quick|summary|overview|explain|deep dive)\b|(ละเอียด|สรุป|เจาะลึก|ย่อ|詳しく|詳細|要約|자세히|요약|상세)/i,
81
+ questions: {
82
+ en: 'How detailed would you like the response to be?',
83
+ th: 'ต้องการคำตอบที่ละเอียดระดับไหน?',
84
+ zh: '您希望回答详细到什么程度?',
85
+ ja: '回答はどの程度の詳しさをご希望ですか?',
86
+ ko: '답변은 어느 정도 자세하게 원하시나요?'
87
+ },
88
+ options: {
89
+ en: ['Very detailed (full analysis)', 'Moderate (key points + details)', 'Brief (quick summary)'],
90
+ th: ['ละเอียดมาก (วิเคราะห์ครบ)', 'ปานกลาง (ประเด็นหลัก + รายละเอียด)', 'กระชับ (สรุปไว)'],
91
+ zh: ['非常详细(完整分析)', '适中(要点 + 细节)', '简短(快速总结)'],
92
+ ja: ['非常に詳しく(完全分析)', '普通(要点 + 詳細)', '簡潔に(クイック要約)'],
93
+ ko: ['매우 자세히 (전체 분석)', '보통 (핵심 + 세부)', '간결하게 (빠른 요약)']
94
+ }
95
+ },
96
+ {
97
+ category: 'comparison',
98
+ weight: 0.7,
99
+ detection: /\b(compare|vs|versus|difference|similar|better|worse|contrast|between)\b|(เปรียบเทียบ|เทียบกับ|ต่างกันยังไง|比較|違い|비교|차이)/i,
100
+ questions: {
101
+ en: 'Which specific items would you like me to compare?',
102
+ th: 'ต้องการให้เปรียบเทียบรายการใดเป็นพิเศษ?',
103
+ zh: '您希望我比较哪些具体项目?',
104
+ ja: '具体的にどの項目を比較しましょうか?',
105
+ ko: '구체적으로 어떤 항목을 비교해 드릴까요?'
106
+ },
107
+ options: {
108
+ en: ['Let me specify', 'Compare all available', 'Focus on top items'],
109
+ th: ['จะระบุเอง', 'เปรียบเทียบทั้งหมด', 'เน้นเฉพาะผลงานเด่น'],
110
+ zh: ['我来指定', '比较全部', '聚焦头部作品'],
111
+ ja: ['自分で指定する', 'すべて比較', '人気作に絞る'],
112
+ ko: ['직접 지정', '전체 비교', '대표작 집중']
113
+ }
114
+ },
115
+ {
116
+ category: 'media_type',
117
+ weight: 0.8,
118
+ detection: /\b(songs?|music|videos?|dramas?|works?|albums?|singles?|ost|stage|performance|mv)\b|(เพลง|ซีรีส์|ละคร|ผลงาน|สเตจ|คอนเสิร์ต|ドラマ|曲|歌|アルバム|シングル|노래|음악|드라마|앨범|작품)/i,
119
+ questions: {
120
+ en: 'Are you looking for her acting works (dramas) or music works?',
121
+ th: 'สนใจผลงานการแสดงซีรีส์ หรือผลงานเพลงเป็นพิเศษคะ?',
122
+ zh: '您是想了解影视剧作品还是音乐作品?',
123
+ ja: '演技作品(ドラマ)と音楽作品のどちらにご興味がありますか?',
124
+ ko: '연기 작품(드라마)과 음악 작품 중 어느 쪽에 관심이 있으신가요?'
125
+ },
126
+ options: {
127
+ en: ['Dramas', 'Music & OSTs', 'All types'],
128
+ th: ['Dramas (ซีรีส์ / ละคร)', 'Music (ผลงานเพลง / OST)', 'All types (รวมทุกประเภท)'],
129
+ zh: ['影视剧作品', '音乐与 OST', '全部类型'],
130
+ ja: ['ドラマ作品', '楽曲・OST', 'すべて'],
131
+ ko: ['드라마 작품', '음악·OST', '전체 유형']
132
+ }
133
+ },
134
+ {
135
+ category: 'language',
136
+ weight: 0.4,
137
+ detection: /\b(chinese|thai|english|cn|th|en)\b|(中文|ไทย|ภาษาไทย|ภาษาจีน|ภาษาอังกฤษ|日本語|韓国語|タイ語|中国語|일본어|한국어|태국어)/i,
138
+ questions: {
139
+ en: 'Would you prefer the response in a specific language?',
140
+ th: 'ต้องการคำตอบเป็นภาษาใด?',
141
+ zh: '您希望用哪种语言回答?',
142
+ ja: '回答は特定の言語をご希望ですか?',
143
+ ko: '답변을 특정 언어로 받기를 원하시나요?'
144
+ },
145
+ options: {
146
+ en: ['Match my language', 'Thai', 'Chinese', 'English'],
147
+ th: ['ตามภาษาที่ฉันใช้', 'ไทย', 'จีน', 'อังกฤษ'],
148
+ zh: ['跟随我的语言', '泰语', '中文', '英语'],
149
+ ja: ['この会話の言語に合わせる', 'タイ語', '中国語', '英語'],
150
+ ko: ['사용 중인 언어에 맞춤', '태국어', '중국어', '영어']
151
+ }
152
+ },
153
+ {
154
+ category: 'specific_work',
155
+ weight: 0.8,
156
+ detection: /(?:\b(drama|series|movie|film|show|showcase)\b|(ซีรีส์|ละคร|หนัง|ドラマ|映画|드라마|영화)).*(?:\b(which|what|tell|about|info|recommend)\b|(เรื่องไหน|แนะนำ|มีเรื่อง|เรื่องอะไร|どの|おすすめ|어느|추천))/i,
157
+ questions: {
158
+ en: 'Which specific drama or work are you interested in?',
159
+ th: 'สนใจซีรีส์หรือผลงานเรื่องใดเป็นพิเศษ?',
160
+ zh: '您对哪部具体的电视剧或作品感兴趣?',
161
+ ja: 'どのドラマや作品にご興味がありますか?',
162
+ ko: '어떤 드라마나 작품에 관심이 있으신가요?'
163
+ },
164
+ options: {
165
+ en: ['In Blossom (花间令)', 'Legend of Yunxi (芸汐传)', 'Sword and Fairy 4 (仙剑四)', 'All works'],
166
+ th: ['In Blossom (ฮวาเจียนลิ่ง)', 'Legend of Yunxi (ตำนานหยุนซี)', 'Sword and Fairy 4 (เซียนกระบี่ 4)', 'All works (ดูซีรีส์ทั้งหมด)'],
167
+ zh: ['《花间令》', '《芸汐传》', '《仙剑四》', '全部作品'],
168
+ ja: ['『花間令』(In Blossom)', '『芸汐伝』(Legend of Yunxi)', '『仙剣四』(Sword and Fairy 4)', 'すべての作品'],
169
+ ko: ['In Blossom (화간령)', 'Legend of Yunxi (윤희전)', 'Sword and Fairy 4 (선검사)', '전체 작품']
170
+ }
171
+ },
172
+ {
173
+ category: 'aspect_focus',
174
+ weight: 0.6,
175
+ detection: /\b(role|character|actor|acting|perform|portray)\b|(บทบาท|ตัวละคร|การแสดง|เบื้องหลัง|役|演技|配役|배역|연기|캐릭터)/i,
176
+ questions: {
177
+ en: 'Would you like to focus on the acting performance, character analysis, or behind-the-scenes details?',
178
+ th: 'ต้องการเน้นที่การแสดง วิเคราะห์ตัวละคร หรือเบื้องหลัง?',
179
+ zh: '您想关注表演分析、角色解读还是幕后花絮?',
180
+ ja: '演技の評価、キャラクター分析、メイキングのどれに注目しますか?',
181
+ ko: '연기력, 캐릭터 분석, 비하인드 중 어느 쪽에 집중할까요?'
182
+ },
183
+ options: {
184
+ en: ['Acting performance', 'Character analysis', 'Behind-the-scenes', 'All aspects'],
185
+ th: ['การแสดง', 'วิเคราะห์ตัวละคร', 'เบื้องหลัง', 'ทุกด้าน'],
186
+ zh: ['演技表现', '角色分析', '幕后花絮', '全部'],
187
+ ja: ['演技', 'キャラクター分析', 'メイキング・舞台裏', 'すべて'],
188
+ ko: ['연기', '캐릭터 분석', '비하인드', '전체']
189
+ }
190
+ },
191
+ {
192
+ category: 'date_range',
193
+ weight: 0.7,
194
+ detection: /\b(between|from|to|until|since|during|before|after|period|era)\b|(ช่วงปี|ตั้งแต่|ถึงปี|ยุค|期間|時代|年代|기간|시대|연도)/i,
195
+ questions: {
196
+ en: 'Could you specify the exact date range or period you\'re interested in?',
197
+ th: 'กรุณาระบุช่วงวันที่หรือระยะเวลาที่ชัดเจน',
198
+ zh: '请具体说明您感兴趣的日期范围或时期',
199
+ ja: '正確な期間や時代をご指定いただけますか?',
200
+ ko: '정확한 기간이나 시대를 알려주시겠어요?'
201
+ },
202
+ options: {
203
+ en: ['2024-2026', '2020-2024', 'Career total'],
204
+ th: ['ปี 2024-2026', 'ปี 2020-2024', 'ตลอดการงานอาชีพ'],
205
+ zh: ['2024-2026 年', '2020-2024 年', '出道至今'],
206
+ ja: ['2024-2026年', '2020-2024年', 'キャリア全体'],
207
+ ko: ['2024-2026년', '2020-2024년', '데뷔 전체']
208
+ }
209
+ },
210
+ {
211
+ category: 'content_format',
212
+ weight: 0.5,
213
+ detection: /\b(list|table|chart|summary|bullet|paragraph|essay|short|long)\b|(ตาราง|สรุป|ลิสต์|ข้อความสั้น|表|箇条書き|リスト|표|목록|글머리)/i,
214
+ questions: {
215
+ en: 'What format would you prefer for the response?',
216
+ th: 'ต้องการรูปแบบคำตอบแบบใด?',
217
+ zh: '您希望回答采用什么格式?',
218
+ ja: '回答はどの形式がよろしいですか?',
219
+ ko: '답변은 어떤 형식을 원하시나요?'
220
+ },
221
+ options: {
222
+ en: ['Detailed paragraphs', 'Bullet points', 'Summary table'],
223
+ th: ['ย่อหน้าละเอียด', 'รายการหัวข้อ (bullet)', 'ตารางสรุป'],
224
+ zh: ['详细段落', '要点列表', '汇总表格'],
225
+ ja: ['詳しい段落', '箇条書き', 'まとめ表'],
226
+ ko: ['상세 문단', '글머리 기호', '요약 표']
227
+ }
228
+ },
229
+ {
230
+ category: 'career_phase',
231
+ weight: 0.75,
232
+ detection: /\b(snh48|snh|group|solo|idol)\b|(วง|เดี่ยว|ไอดอล|ยุควง|ช่วงวง|ソロ|アイドル|グループ|솔로|아이돌|그룹)/i,
233
+ questions: {
234
+ en: 'Are you interested in her Solo artist works or SNH48 idol group era?',
235
+ th: 'สนใจผลงานช่วงศิลปินเดี่ยว (Solo) หรือช่วงไอดอลวง SNH48 คะ?',
236
+ zh: '请问您想了解个人 Solo 时期还是 SNH48 组合时期?',
237
+ ja: 'ソロアーティスト時代と SNH48 アイドルグループ時代、どちらに興味がありますか?',
238
+ ko: '솔로 아티스트 시절과 SNH48 그룹 시절 중 어느 쪽에 관심이 있으신가요?'
239
+ },
240
+ options: {
241
+ en: ['Solo works', 'SNH48 era', 'All eras'],
242
+ th: ['Solo works (ผลงานเดี่ยว)', 'SNH48 era (ช่วงวง SNH48)', 'All eras (ทุกช่วงเวลา)'],
243
+ zh: ['个人 Solo 时期', 'SNH48 组合时期', '全部时期'],
244
+ ja: ['ソロ活動', 'SNH48 時代', '全期間'],
245
+ ko: ['솔로 활동', 'SNH48 시절', '전체 시기']
246
+ }
247
+ }
248
+ ];
249
+
250
+ // Resolve a pattern's question + options for a given language with English fallback.
251
+ function localizePattern(pattern, lang) {
252
+ const questions = pattern.questions || {};
253
+ const options = pattern.options || {};
254
+ return {
255
+ category: pattern.category,
256
+ weight: pattern.weight,
257
+ question: questions[lang] || questions.en,
258
+ options: options[lang] || options.en || [],
259
+ };
260
+ }
261
+
262
+ export function calculateConfidence(query, detectedAmbiguities = []) {
263
+ if (!query || typeof query !== 'string') {
264
+ return { score: 0, level: 'very_low', shouldAsk: true };
265
+ }
266
+
267
+ const trimmed = query.trim();
268
+ let score = 1.0;
269
+
270
+ // CJK scripts do not separate words with spaces, so estimate word count from
271
+ // character length instead to avoid unfairly penalizing zh/ja/ko queries.
272
+ const cjkChars = (trimmed.match(/[\u3040-\u30FF\u3400-\u9FFF\uAC00-\uD7AF]/g) || []).length;
273
+ const wordCount = cjkChars > 0
274
+ ? Math.max(1, Math.round(cjkChars / 2))
275
+ : trimmed.split(/\s+/).length;
276
+ if (wordCount < 3) score -= 0.3;
277
+ else if (wordCount < 5) score -= 0.1;
278
+ else if (wordCount > 15) score += 0.1;
279
+
280
+ const knownWorksOrCharacters = /(花间令|花間令|In Blossom|ฮวาเจียนลิ่ง|芸汐传|芸汐伝|Yunxi|หยุนซี|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|The Blooms at Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|杨采薇|上官芷|หยางไฉ่เวย|ซ่างกวนจื่อ|韩芸汐|หานหยุนซี|白素贞|ไป๋ซู่เจิน|傅容|ฟู่หรง|姜保宁|เจียงเป่าหนิง|韩菱纱|หานหลิงซา)/i;
281
+ if (knownWorksOrCharacters.test(trimmed)) {
282
+ return {
283
+ score: 0.95,
284
+ level: 'high',
285
+ shouldAsk: false,
286
+ reasoning: 'High confidence: specific work or character explicitly identified in query.'
287
+ };
288
+ }
289
+
290
+ const specificityPatterns = [
291
+ /\b(exactly|specifically|precisely|particular)\b/i,
292
+ /\b\d{4}\b/,
293
+ /["'"]/,
294
+ /\b(by|from|in|at|on)\b.*\b(Ju Jingyi|鞠婧祎|จวีจิ้งอี)\b/i,
295
+ ];
296
+
297
+ for (const pattern of specificityPatterns) {
298
+ if (pattern.test(trimmed)) score += 0.15;
299
+ }
300
+
301
+ if (/^(what|who|when|where|how|which|why|tell me|show me|list|compare)\b/i.test(trimmed)) {
302
+ score += 0.1;
303
+ }
304
+
305
+ for (const ambiguity of detectedAmbiguities) {
306
+ score -= (ambiguity.weight || 0.5) * 0.3;
307
+ }
308
+
309
+ score = Math.max(0, Math.min(1, score));
310
+
311
+ let level;
312
+ if (score >= 0.8) level = 'high';
313
+ else if (score >= 0.6) level = 'medium';
314
+ else if (score >= 0.4) level = 'low';
315
+ else level = 'very_low';
316
+
317
+ return {
318
+ score,
319
+ level,
320
+ shouldAsk: score < 0.65,
321
+ reasoning: `Confidence: ${(score * 100).toFixed(0)}% (${level}). ${detectedAmbiguities.length} ambiguities detected.`
322
+ };
323
+ }
324
+
325
+ export function detectAmbiguities(query, detectedLang = 'en') {
326
+ if (!query || typeof query !== 'string') return [];
327
+
328
+ const lang = normalizeLang(detectedLang);
329
+ const detected = [];
330
+ const seenCategories = new Set();
331
+
332
+ for (const pattern of AMBIGUITY_PATTERNS) {
333
+ if (seenCategories.has(pattern.category)) continue;
334
+
335
+ if (pattern.detection.test(query)) {
336
+ seenCategories.add(pattern.category);
337
+ detected.push({
338
+ category: pattern.category,
339
+ question: pattern.questions[lang] || pattern.questions.en,
340
+ options: pattern.options[lang] || pattern.options.en,
341
+ weight: pattern.weight,
342
+ });
343
+ }
344
+ }
345
+
346
+ return detected;
347
+ }
348
+
349
+ // Known Ju Jingyi works/characters so clarifying questions can reference the
350
+ // actual subject of the user's query instead of generic facet questions.
351
+ const KNOWN_ENTITY_PATTERNS = [
352
+ {
353
+ key: 'in_blossom',
354
+ labels: { en: 'In Blossom (花间令)', th: 'อินบลอสซัม/ฮวาเจียนหลิง (In Blossom)', zh: '《花间令》', ja: '『花間令』(In Blossom)', ko: 'In Blossom (화간령)' },
355
+ },
356
+ {
357
+ key: 'legend_of_yunxi',
358
+ labels: { en: 'Legend of Yunxi (芸汐传)', th: 'ตำนานหยุนซี (Legend of Yunxi)', zh: '《芸汐传》', ja: '『芸汐伝』(Legend of Yunxi)', ko: 'Legend of Yunxi (윤희전)' },
359
+ },
360
+ {
361
+ key: 'sword_fairy_4',
362
+ labels: { en: 'Sword and Fairy 4 (仙剑四)', th: 'เซียนกระบี่พิชิตมาร 4 (Sword and Fairy 4)', zh: '《仙剑四》', ja: '『仙剣四』(Sword and Fairy 4)', ko: 'Sword and Fairy 4 (선검사)' },
363
+ },
364
+ {
365
+ key: 'white_snake',
366
+ labels: { en: 'New Legend of Madame White Snake (新白娘子传奇)', th: 'นางพญางูขาว (New Legend of Madame White Snake)', zh: '《新白娘子传奇》', ja: '『新白娘子伝奇』(White Snake)', ko: 'New Legend of Madame White Snake (신백납자전기)' },
367
+ },
368
+ {
369
+ key: 'ruyi_pavilion',
370
+ labels: { en: 'The Blooms at Ruyi Pavilion (如意芳霏)', th: 'กรุ่นรักกลิ่นบุปผา (The Blooms at Ruyi Pavilion)', zh: '《如意芳霏》', ja: '『如意芳霏』(Ruyi Pavilion)', ko: 'The Blooms at Ruyi Pavilion (여의방비)' },
371
+ },
372
+ {
373
+ key: 'rebirth_for_you',
374
+ labels: { en: 'Rebirth For You (嘉南传)', th: 'กำเนิดใหม่เพื่อเธอ (Rebirth For You)', zh: '《嘉南传》', ja: '『嘉南伝』(Rebirth For You)', ko: 'Rebirth For You (가남전)' },
375
+ },
376
+ {
377
+ key: 'snh48',
378
+ labels: { en: 'SNH48 era', th: 'ยุควง SNH48', zh: 'SNH48 时期', ja: 'SNH48 時代', ko: 'SNH48 시절' },
379
+ },
380
+ ];
381
+
382
+ export function stripSystemDirectives(text) {
383
+ if (!text || typeof text !== 'string') return '';
384
+ return text
385
+ .replace(/\[SYSTEM\s+[A-Z\s()&_-]+:[\s\S]*?\]/gi, '')
386
+ .replace(/\[SYSTEM:[^\]]*\]/gi, '')
387
+ .replace(/\[SYSTEM\s+NOTE:[^\]]*\]/gi, '')
388
+ .replace(/\[SYSTEM\s+INSTRUCTION:[^\]]*\]/gi, '')
389
+ .replace(/\[SYSTEM\s+DIRECTIVE:[^\]]*\]/gi, '')
390
+ .trim();
391
+ }
392
+
393
+ const KNOWN_ENTITY_REGEX = /(花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง|芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|SNH48)/i;
394
+
395
+ // Returns a recognized topic label (localized) that appears in the query, or null.
396
+ function extractQueryTopic(query, lang) {
397
+ if (!query || typeof query !== 'string') return null;
398
+ const clean = stripSystemDirectives(query);
399
+ const hit = KNOWN_ENTITY_PATTERNS.find(p => {
400
+ try { return new RegExp(escapeRegex(p.key) + '|' + entityRegexFor(p.key), 'i').test(clean); } catch { return false; }
401
+ });
402
+ if (hit) return (hit.labels && hit.labels[lang]) || hit.labels.en;
403
+ const rawMatch = clean.match(KNOWN_ENTITY_REGEX);
404
+ if (rawMatch) return rawMatch[0];
405
+ return null;
406
+ }
407
+
408
+ function entityRegexFor(key) {
409
+ const map = {
410
+ in_blossom: '花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง',
411
+ legend_of_yunxi: '芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี',
412
+ sword_fairy_4: '仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่',
413
+ white_snake: '新白娘子传奇|White Snake|นางพญางูขาว',
414
+ ruyi_pavilion: '如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา',
415
+ rebirth_for_you: '嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ',
416
+ snh48: 'SNH48',
417
+ };
418
+ return map[key] || '';
419
+ }
420
+
421
+ function escapeRegex(str) {
422
+ return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
423
+ }
424
+
425
+ // Extract a short noun phrase that likely represents the topic (fallback when no known entity).
426
+ function extractQueryPhrase(query) {
427
+ if (!query || typeof query !== 'string') return '';
428
+ const cleaned = stripSystemDirectives(query)
429
+ .replace(/\b(please|pls|tell me|show me|give me|about|regarding|hello|hi|hey)\b/gi, ' ')
430
+ .replace(/[?!。.,,]+$/g, '')
431
+ .trim();
432
+ const tokens = cleaned.split(/\s+/).filter(Boolean);
433
+ if (tokens.length <= 4) return cleaned;
434
+ return tokens.slice(0, 4).join(' ');
435
+ }
436
+
437
+ function synthesizeDynamicQuestions(query, lang = 'en', detectedAmbiguities = [], maxQuestions = 4) {
438
+ const results = [];
439
+ const addedCategories = new Set();
440
+
441
+ const addQ = (category, question, options) => {
442
+ if (!addedCategories.has(category) && results.length < maxQuestions) {
443
+ addedCategories.add(category);
444
+ results.push({ category, question, options });
445
+ }
446
+ };
447
+
448
+ const topic = extractQueryTopic(query, lang) || extractQueryPhrase(query) || null;
449
+
450
+ // 1) Subject-aware questions that reference the actual topic the user asked about,
451
+ // so the clarifying questions feel directly connected to the query.
452
+ if (topic) {
453
+ const subject = String(topic).trim();
454
+ if (!addedCategories.has('subject_focus') && results.length < maxQuestions) {
455
+ if (lang === 'th') {
456
+ addQ('subject_focus', `เกี่ยวกับ "${subject}" นี้ คุณสนใจเจาะลึกด้านใดเป็นพิเศษคะ?`, [
457
+ 'บทบาทและการแสดง',
458
+ 'ตัวละครและเนื้อเรื่อง',
459
+ 'กระแสตอบรับและรางวัล',
460
+ 'รายละเอียดเบื้องหลัง'
461
+ ]);
462
+ } else if (lang === 'zh') {
463
+ addQ('subject_focus', `关于「${subject}」,您最想深入了解哪一方面?`, [
464
+ '角色与演技',
465
+ '故事与人设',
466
+ '口碑与奖项',
467
+ '幕后花絮'
468
+ ]);
469
+ } else if (lang === 'ja') {
470
+ addQ('subject_focus', `「${subject}」について、特にどの面を深掘りしましょうか?`, [
471
+ '演技と役柄',
472
+ 'ストーリーとキャラクター',
473
+ '評価と賞',
474
+ 'メイキング・舞台裏'
475
+ ]);
476
+ } else if (lang === 'ko') {
477
+ addQ('subject_focus', `"${subject}"에 대해 어떤 부분을 가장 깊이 알아볼까요?`, [
478
+ '연기와 배역',
479
+ '스토리와 캐릭터',
480
+ '평가와 수상',
481
+ '비하인드'
482
+ ]);
483
+ } else {
484
+ addQ('subject_focus', `About "${subject}" specifically, which aspect interests you most?`, [
485
+ 'Acting & performance',
486
+ 'Story & characters',
487
+ 'Reception & awards',
488
+ 'Behind-the-scenes'
489
+ ]);
490
+ }
491
+ }
492
+ if (!addedCategories.has('subject_scope') && results.length < maxQuestions && detectedAmbiguities.length === 0) {
493
+ if (lang === 'th') {
494
+ addQ('subject_scope', `ต้องการเน้นเฉพาะ "${subject}" นี้ หรือให้เชื่อมโยงกับผลงานอื่นๆ ของจวีจิ้งอีด้วยคะ?`, [
495
+ `เฉพาะ "${subject}"`,
496
+ 'เชื่อมโยงกับผลงานอื่น',
497
+ 'ภาพรวมทั้งหมด'
498
+ ]);
499
+ } else if (lang === 'zh') {
500
+ addQ('subject_scope', `您希望只围绕「${subject}」,还是也想关联她的其他作品?`, [
501
+ `仅「${subject}」`,
502
+ '关联其他作品',
503
+ '整体概述'
504
+ ]);
505
+ } else if (lang === 'ja') {
506
+ addQ('subject_scope', `「${subject}」だけに絞りますか、それとも他の作品とも絡めて紹介しましょうか?`, [
507
+ `「${subject}」のみ`,
508
+ '他の作品とも関連付け',
509
+ '全体概観'
510
+ ]);
511
+ } else if (lang === 'ko') {
512
+ addQ('subject_scope', `"${subject}"에만 집중할까요, 아니면 다른 작품과도 연결해서 알아볼까요?`, [
513
+ `"${subject}"만`,
514
+ '다른 작품과도 연결',
515
+ '전체 개요'
516
+ ]);
517
+ } else {
518
+ addQ('subject_scope', `Should we focus only on "${subject}" or also relate it to her other works?`, [
519
+ `Only "${subject}"`,
520
+ 'Relate to other works',
521
+ 'Overall overview'
522
+ ]);
523
+ }
524
+ }
525
+ }
526
+
527
+ // 2) Specific detected ambiguity categories (from the query).
528
+ if (Array.isArray(detectedAmbiguities) && detectedAmbiguities.length > 0) {
529
+ for (const amb of detectedAmbiguities) {
530
+ if (amb.category && amb.question && amb.options && !addedCategories.has(amb.category)) {
531
+ addQ(amb.category, amb.question, (amb.options || []).slice(0, 4));
532
+ }
533
+ }
534
+ }
535
+
536
+ // 3) Generic facet fillers to reach the target count.
537
+ if (lang === 'th') {
538
+ addQ('media_type', 'อยากให้เน้นผลงานการแสดงซีรีส์ หรือผลงานเพลงเดี่ยวและเพลงประกอบ (OST) คะ?', [
539
+ 'ผลงานซีรีส์ (Dramas)',
540
+ 'ผลงานเพลงเดี่ยวและ OST',
541
+ 'รวมทุกประเภทผลงาน'
542
+ ]);
543
+ addQ('career_phase', 'คุณสนใจผลงานช่วงศิลปินเดี่ยวและนักแสดงในปัจจุบัน หรือช่วงยุคไอดอลวง SNH48 คะ?', [
544
+ 'ศิลปินเดี่ยวปัจจุบัน (Solo)',
545
+ 'ช่วงยุควง SNH48',
546
+ 'ทุกช่วงเวลาทั้งหมด'
547
+ ]);
548
+ addQ('timeframe', 'ต้องการให้อัปเดตผลงานช่วงล่าสุด (ปี 2024-2026) หรือดูผลงานสร้างชื่อในอดีตคะ?', [
549
+ 'ผลงานล่าสุด (2024-2026)',
550
+ 'ผลงานสร้างชื่อในอดีต',
551
+ 'ผลงานทั้งหมดตลอดกาล'
552
+ ]);
553
+ addQ('depth', 'ต้องการคำตอบแบบสรุปประเด็นสำคัญ หรือเจาะลึกรายละเอียดและเรื่องย่อเบื้องหลังคะ?', [
554
+ 'สรุปกระชับ ได้ใจความ',
555
+ 'เจาะลึกละเอียด ครบถ้วน',
556
+ 'ไฮไลต์ยอดนิยม'
557
+ ]);
558
+ if (results.length < maxQuestions) {
559
+ addQ('aspect_focus', 'สนใจเจาะลึกด้านบทบาทการแสดง ตัวละคร หรือกระแสตอบรับและรางวัลคะ?', [
560
+ 'บทบาทและการแสดง',
561
+ 'ตัวละครและเนื้อเรื่อง',
562
+ 'กระแสตอบรับและรางวัล'
563
+ ]);
564
+ }
565
+ } else if (lang === 'zh') {
566
+ addQ('media_type', '请问您希望重点了解影视剧作品,还是个人单曲与影视原声 (OST)?', [
567
+ '影视剧作品 (Dramas)',
568
+ '个人单曲与 OST',
569
+ '全方位作品介绍'
570
+ ]);
571
+ addQ('career_phase', '请问您想了解个人 Solo 时期作品,还是 SNH48 组合时期的舞台?', [
572
+ '个人 Solo 时期作品',
573
+ 'SNH48 组合时期',
574
+ '全部演艺阶段'
575
+ ]);
576
+ addQ('timeframe', '请问您想了解近年 (2024-2026) 的最新动态,还是历年经典代表作?', [
577
+ '最新动态 (2024-2026)',
578
+ '历年经典代表作',
579
+ '历年全部作品'
580
+ ]);
581
+ addQ('depth', '您希望获取快速简练的重点总结,还是包含剧情与幕后细节的深度解读?', [
582
+ '精简要点总结',
583
+ '深度详尽解读',
584
+ '热门高光盘点'
585
+ ]);
586
+ if (results.length < maxQuestions) {
587
+ addQ('aspect_focus', '您想重点了解角色演技、故事设定还是作品热度与奖项?', [
588
+ '角色表现与演技',
589
+ '故事与人设解读',
590
+ '热度口碑与奖项'
591
+ ]);
592
+ }
593
+ } else if (lang === 'ja') {
594
+ addQ('media_type', '演技ドラマ作品と、ソロ楽曲や映画・ドラマの主題歌 (OST)、どちらを重視しましょうか?', [
595
+ 'ドラマ作品',
596
+ '楽曲・OST',
597
+ 'すべての作品'
598
+ ]);
599
+ addQ('career_phase', '現在のソロ活動と、SNH48 アイドルグループ時代のどちらが気になりますか?', [
600
+ '現在のソロ活動',
601
+ 'SNH48 時代',
602
+ '全期間'
603
+ ]);
604
+ addQ('timeframe', '最近の活動 (2024-2026) と、過去の代表作のどちらをご覧になりたいですか?', [
605
+ '最新の活動 (2024-2026)',
606
+ '過去の代表作',
607
+ '全期間の作品'
608
+ ]);
609
+ addQ('depth', '簡潔な要約と、詳細な解説のどちらをご希望ですか?', [
610
+ '簡潔な要約',
611
+ '詳細な解説',
612
+ '人気ハイライト'
613
+ ]);
614
+ if (results.length < maxQuestions) {
615
+ addQ('aspect_focus', '演技、キャラクター、評価・賞のどれを深掘りしましょうか?', [
616
+ '演技と役柄',
617
+ 'キャラクターとストーリー',
618
+ '評価と賞'
619
+ ]);
620
+ }
621
+ } else if (lang === 'ko') {
622
+ addQ('media_type', '연기 드라마 작품과 솔로 곡·주제가 (OST) 중 어떤 것을 중점적으로 볼까요?', [
623
+ '드라마 작품',
624
+ '솔로 곡·OST',
625
+ '전체 작품'
626
+ ]);
627
+ addQ('career_phase', '현재 솔로 활동과 SNH48 아이돌 그룹 시절 중 어떤 시기가 궁금하신가요?', [
628
+ '현재 솔로 활동',
629
+ 'SNH48 시절',
630
+ '전체 시기'
631
+ ]);
632
+ addQ('timeframe', '최근 활동 (2024-2026)과 대표작 중 어떤 것을 볼까요?', [
633
+ '최근 활동 (2024-2026)',
634
+ '대표작',
635
+ '전체 작품'
636
+ ]);
637
+ addQ('depth', '간결한 요약과 상세한 해설 중 어떤 걸 원하시나요?', [
638
+ '간결한 요약',
639
+ '상세한 해설',
640
+ '인기 하이라이트'
641
+ ]);
642
+ if (results.length < maxQuestions) {
643
+ addQ('aspect_focus', '연기, 캐릭터, 평가·수상 중 어떤 부분을 깊이 알아볼까요?', [
644
+ '연기와 배역',
645
+ '캐릭터와 스토리',
646
+ '평가와 수상'
647
+ ]);
648
+ }
649
+ } else {
650
+ addQ('media_type', 'Would you like to focus on her acting dramas, or her solo music and OSTs?', [
651
+ 'Acting Dramas',
652
+ 'Solo Music & OSTs',
653
+ 'All Works'
654
+ ]);
655
+ addQ('career_phase', 'Are you interested in her current Solo career or her SNH48 idol group era?', [
656
+ 'Current Solo works',
657
+ 'SNH48 group era',
658
+ 'All career eras'
659
+ ]);
660
+ addQ('timeframe', 'Would you prefer updates on her recent works (2024-2026) or her classic breakthrough hits?', [
661
+ 'Recent works (2024-2026)',
662
+ 'Classic breakthrough hits',
663
+ 'All time works'
664
+ ]);
665
+ addQ('depth', 'Would you prefer a concise quick summary or an in-depth comprehensive breakdown?', [
666
+ 'Concise summary',
667
+ 'In-depth breakdown',
668
+ 'Key highlights'
669
+ ]);
670
+ if (results.length < maxQuestions) {
671
+ addQ('aspect_focus', 'Would you like to focus on character portrayals, storylines, or reception & awards?', [
672
+ 'Character & Acting',
673
+ 'Story & Lore',
674
+ 'Reception & Awards'
675
+ ]);
676
+ }
677
+ }
678
+
679
+ return results.slice(0, maxQuestions);
680
+ }
681
+
682
+ function extractJsonArray(text) {
683
+ if (!text || typeof text !== 'string') return null;
684
+ let cleaned = text.trim();
685
+ const fenceMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
686
+ if (fenceMatch) {
687
+ cleaned = fenceMatch[1].trim();
688
+ }
689
+ const firstBracket = cleaned.indexOf('[');
690
+ const lastBracket = cleaned.lastIndexOf(']');
691
+ if (firstBracket !== -1 && lastBracket !== -1 && lastBracket > firstBracket) {
692
+ const candidate = cleaned.slice(firstBracket, lastBracket + 1);
693
+ try {
694
+ const parsed = JSON.parse(candidate);
695
+ if (Array.isArray(parsed) && parsed.length > 0) {
696
+ return parsed;
697
+ }
698
+ } catch {
699
+ try {
700
+ const repaired = candidate.replace(/,\s*([\]}])/g, '$1');
701
+ const parsed = JSON.parse(repaired);
702
+ if (Array.isArray(parsed) && parsed.length > 0) {
703
+ return parsed;
704
+ }
705
+ } catch { }
706
+ }
707
+ }
708
+ return null;
709
+ }
710
+
711
+ const LLM_SYSTEM_PROMPTS = {
712
+ th:
713
+ 'คุณคือ Kiku AI ระบบผู้ช่วยอัจฉริยะทางการของ "จวีจิ้งอี" (Ju Jingyi / 鞠婧祎 / เสี่ยงจวี)\n' +
714
+ 'หน้าที่: วิเคราะห์คำถามของผู้ใช้ และคิดคำถามเจาะจง 3-4 ข้ออย่างเป็นมิตร สุภาพ (ลงท้ายด้วย คะ/นะคะ) เพื่อถามกลับผู้ใช้ก่อนจะเริ่มค้นหาข้อมูลหรือเริ่มกระบวนการ Timeline Agent\n' +
715
+ 'เป้าหมาย:\n' +
716
+ '1. เจาะลึกความต้องการให้แคบลงและตรงกับสิ่งที่ผู้ใช้ถามจริง (เช่น บทบาท/ตัวละคร, ผลงานซีรีส์ vs เพลง, ช่วงปี พ.ศ./ค.ศ., ยุคปัจจุบัน vs ไอดอล SNH48, ความละเอียดของข้อมูล)\n' +
717
+ '2. แต่ละข้อต้องให้ตัวเลือกคำตอบ 2-4 ตัวเลือกที่เกี่ยวข้องโดยตรงเพื่อให้ผู้ใช้กดเลือกได้ทันที\n' +
718
+ '3. ห้ามพูดนอกเรื่อง หรือแต่งประโยคระบบคำสั่งลงในคำถาม\n' +
719
+ '4. ส่งคำตอบเป็น JSON Array เท่านั้น ห้ามใส่คำอธิบายอื่นนอกเหนือจาก JSON:\n' +
720
+ '[{"category": "subject_focus", "question": "คำถามภาษาไทย", "options": ["ตัวเลือก 1", "ตัวเลือก 2", "ตัวเลือก 3"]}]',
721
+ zh:
722
+ '你是鞠婧祎官方智能粉丝助手 Kiku。\n' +
723
+ '任务:根据用户的提问,主动思考并提出 3-4 个礼貌贴切的追问(针对具体作品、角色、音乐还是影视、年份阶段、SNH48时期还是Solo、详略程度等不同维度),以便在进入 Timeline Agent 检索前准确定位用户需求。\n' +
724
+ '每个追问提供 2-4 个清晰选项。\n' +
725
+ '仅输出合法的 JSON Array 格式,不要包含任何多余文字:\n' +
726
+ '[{"category": "subject_focus", "question": "中文追问", "options": ["选项1", "选项2", "选项3"]}]',
727
+ ja:
728
+ 'あなたは鞠婧祎(Ju Jingyi)公式ファンアシスタント「Kiku AI」です。\n' +
729
+ 'タスク:ユーザーの質問を分析し、Timeline Agent が検索を始める前に意図を正確に把握できるよう、3〜4件の丁寧で的確な確認質問を日本語で生成してください(具体的な作品・役柄、ドラマか音楽か、Solo か SNH48 か、時期、詳しさなどの観点)。\n' +
730
+ '各質問には2〜4個の選択肢を付けてください。\n' +
731
+ 'JSON配列のみを出力し、他の文章は含めないでください:\n' +
732
+ '[{"category": "subject_focus", "question": "日本語の質問", "options": ["選択肢1", "選択肢2", "選択肢3"]}]',
733
+ ko:
734
+ '당신은 Ju Jingyi(鞠婧祎) 공식 팬 어시스턴트 Kiku AI입니다.\n' +
735
+ '임무: 사용자의 질문을 분석하여 Timeline Agent가 검색을 시작하기 전에 의도를 정확히 파악할 수 있도록 3-4개의 정중한 확인 질문을 한국어로 생성하세요(구체적 작품·배역, 드라마 vs 음악, Solo vs SNH48, 시기, 상세도 등의 관점).\n' +
736
+ '각 질문에는 2-4개의 선택지를 제공하세요.\n' +
737
+ 'JSON 배열만 출력하고 다른 문장은 포함하지 마세요:\n' +
738
+ '[{"category": "subject_focus", "question": "한국어 질문", "options": ["선택지1", "선택지2", "선택지3"]}]',
739
+ en:
740
+ 'You are Kiku AI, the official assistant for Ju Jingyi (鞠婧祎).\n' +
741
+ 'Task: Analyze the user inquiry and generate 3-4 polite, targeted clarifying questions across relevant dimensions (specific role/work, drama vs music, solo vs SNH48, timeframe, depth) to pinpoint their intent before the Timeline Agent begins searching.\n' +
742
+ 'Provide 2-4 concise clickable options per question.\n' +
743
+ 'Output ONLY a valid JSON Array with no other text:\n' +
744
+ '[{"category": "subject_focus", "question": "Question?", "options": ["Option 1", "Option 2", "Option 3"]}]',
745
+ };
746
+
747
+ // Shown when a model-generated question arrives without usable options.
748
+ const FALLBACK_OPTIONS = {
749
+ en: ['Yes', 'No', 'All'],
750
+ th: ['ใช่', 'ไม่ใช่', 'ทั้งหมด'],
751
+ zh: ['是', '不是', '全部'],
752
+ ja: ['はい', 'いいえ', 'すべて'],
753
+ ko: ['네', '아니요', '전체'],
754
+ };
755
+
756
+ export async function generateClarifyingQuestions(query, lang = 'en', options = {}) {
757
+ const { conversationHistory = [], client = null, maxQuestions = 4, detectedAmbiguities = [] } = options;
758
+ const cleanQuery = stripSystemDirectives(query) || (typeof query === 'string' ? query.trim() : '');
759
+ const normalizedLang = normalizeLang(lang);
760
+
761
+ if (client && typeof client.chat === 'function') {
762
+ const recentHistory = conversationHistory
763
+ .slice(-4)
764
+ .map(msg => `${msg.role}: ${(stripSystemDirectives(msg.content) || msg.content || '').slice(0, 150)}`)
765
+ .filter(line => !line.startsWith('system:'))
766
+ .join('\n');
767
+
768
+ const ambiguityAreas = detectedAmbiguities.map(a => a.category).join(', ');
769
+
770
+ const systemPrompt = LLM_SYSTEM_PROMPTS[normalizedLang] || LLM_SYSTEM_PROMPTS.en;
771
+
772
+ const userPrompt = `User inquiry: "${cleanQuery}"\n${ambiguityAreas ? `Ambiguity cues: ${ambiguityAreas}\n` : ''}${recentHistory ? `Recent history:\n${recentHistory}` : ''}\n\nFormulate 3-4 targeted clarifying questions as JSON array:`;
773
+
774
+ try {
775
+ const controller = new AbortController();
776
+ const timeoutMs = options.timeoutMs || Math.min(client.timeout || 30000, 15000);
777
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
778
+
779
+ const response = await client.chat({
780
+ messages: [
781
+ { role: 'system', content: systemPrompt },
782
+ { role: 'user', content: userPrompt }
783
+ ],
784
+ temperature: 0.3,
785
+ maxTokens: 600,
786
+ stream: false,
787
+ signal: controller.signal
788
+ });
789
+ clearTimeout(timeoutId);
790
+
791
+ const content = response?.content || '';
792
+ const rawQuestions = extractJsonArray(content);
793
+ if (Array.isArray(rawQuestions) && rawQuestions.length >= 2) {
794
+ const fallbackOpts = FALLBACK_OPTIONS[normalizedLang] || FALLBACK_OPTIONS.en;
795
+ const validated = rawQuestions
796
+ .filter(q => q && typeof q.question === 'string' && q.question.trim().length > 0)
797
+ .map((q, idx) => ({
798
+ category: String(q.category || `clarify_${idx + 1}`).toLowerCase().replace(/[^a-z0-9_]/g, '_'),
799
+ question: q.question.trim(),
800
+ options: Array.isArray(q.options) && q.options.length > 0
801
+ ? q.options.map(opt => String(opt).trim()).filter(Boolean).slice(0, 4)
802
+ : [...fallbackOpts]
803
+ }));
804
+
805
+ if (validated.length >= 2) {
806
+ return validated.slice(0, maxQuestions);
807
+ }
808
+ }
809
+ } catch {
810
+ // Fast fallback to dynamic contextual synthesis if model call fails/times out
811
+ }
812
+ }
813
+
814
+ return synthesizeDynamicQuestions(cleanQuery, normalizedLang, detectedAmbiguities, maxQuestions);
815
+ }
816
+
817
+ export function isQueryTooVague(query) {
818
+ if (!query || typeof query !== 'string') return true;
819
+
820
+ const trimmed = query.trim();
821
+
822
+ if (trimmed.length < 3) return true;
823
+
824
+ // Punctuation/symbols only — treat as vague (includes CJK punctuation).
825
+ if (/^[^\w\u0E00-\u0E7F\u3040-\u30FF\u3400-\u9FFF\uAC00-\uD7AF]+$/.test(trimmed)) return true;
826
+
827
+ const words = trimmed.split(/\s+/);
828
+ if (words.length === 1 && words[0].length < 4) return true;
829
+
830
+ return false;
831
+ }
832
+
833
+ export function formatQuestionsForCLI(questions, colors = {}) {
834
+ if (!questions || questions.length === 0) return '';
835
+
836
+ const C = colors;
837
+ const lines = [];
838
+
839
+ lines.push(`${C.gold || ''}┌─ Clarifying Questions ─────────────────────────────┐${C.reset || ''}`);
840
+ lines.push(`${C.gold || ''}│${C.reset || ''} ${C.dim || ''}The agent detected some ambiguity in your query.${C.reset || ''}`);
841
+ lines.push(`${C.gold || ''}│${C.reset || ''} ${C.dim || ''}Answering these will help provide a more accurate response.${C.reset || ''}`);
842
+ lines.push(`${C.gold || ''}│${C.reset || ''}`);
843
+
844
+ questions.forEach((q, index) => {
845
+ lines.push(`${C.gold || ''}│${C.reset || ''} ${C.bold || ''}${C.roseGlow || ''}${index + 1}. ${q.category.toUpperCase()}${C.reset || ''}`);
846
+ lines.push(`${C.gold || ''}│${C.reset || ''} ${C.white || ''}${q.question}${C.reset || ''}`);
847
+ if (q.options && q.options.length > 0) {
848
+ q.options.forEach((opt, i) => {
849
+ lines.push(`${C.gold || ''}│${C.reset || ''} ${C.dim || ''}${String.fromCharCode(97 + i)}) ${opt}${C.reset || ''}`);
850
+ });
851
+ }
852
+ lines.push(`${C.gold || ''}│${C.reset || ''}`);
853
+ });
854
+
855
+ lines.push(`${C.gold || ''}│${C.reset || ''} ${C.dim || ''}Type your answers (e.g., "1a 2c") or "skip" to proceed without clarifying.${C.reset || ''}`);
856
+ lines.push(`${C.gold || ''}└────────────────────────────────────────────────────┘${C.reset || ''}`);
857
+
858
+ return lines.join('\n');
859
+ }
860
+
861
+ const SSE_INSTRUCTIONS = {
862
+ en: 'Answer the questions above to help the agent provide a more accurate response. Type "skip" to proceed without answering.',
863
+ th: 'ตอบคำถามด้านบนเพื่อช่วยให้ AI ตอบได้แม่นยำยิ่งขึ้น หรือพิมพ์ "skip" เพื่อข้ามและตอบตามเดิม',
864
+ zh: '请回答上面的问题,以便助手给出更准确的回答。输入 "skip" 可跳过。',
865
+ ja: '上記の質問に答えると、より正確な回答が届きます。「skip」と入力するとスキップできます。',
866
+ ko: '위 질문에 답하면 더 정확한 답변을 받을 수 있습니다. "skip"을 입력하면 건너뛸 수 있습니다.',
867
+ };
868
+
869
+ export function formatQuestionsForSSE(questions, lang = 'en') {
870
+ if (!questions || questions.length === 0) return null;
871
+
872
+ const normalized = normalizeLang(lang);
873
+ return {
874
+ type: 'clarifying_questions',
875
+ language: normalized,
876
+ questions: questions.map(q => ({
877
+ category: q.category,
878
+ question: q.question,
879
+ options: q.options,
880
+ })),
881
+ instruction: SSE_INSTRUCTIONS[normalized] || SSE_INSTRUCTIONS.en,
882
+ };
883
+ }
884
+
885
+ // Words that mean "skip these questions" across the supported languages.
886
+ const SKIP_WORDS = new Set([
887
+ 'skip', 'next', 'no', 'none', 'pass', 'proceed',
888
+ 'ข้าม', 'ไม่', 'ไม่ต้อง', 'ไม่ต้องถาม', 'ผ่าน',
889
+ '跳过', '不用', '不用了', '算了',
890
+ 'スキップ', '結構です', 'いいえ',
891
+ '건너뛰기', '넘어가기', '괜찮아요', '아니요',
892
+ ]);
893
+
894
+ export function parseClarifyingAnswers(answersText, questions) {
895
+ if (!answersText || typeof answersText !== 'string' || !questions) {
896
+ return { answers: {}, summary: '' };
897
+ }
898
+
899
+ const trimmed = answersText.trim().toLowerCase();
900
+
901
+ if (trimmed === '' || SKIP_WORDS.has(trimmed)) {
902
+ return { answers: {}, summary: 'User chose to skip clarifying questions.' };
903
+ }
904
+
905
+ const answers = {};
906
+ const summaryParts = [];
907
+
908
+ const isStructured = /^[\da-d\s,;]+$/.test(trimmed) && trimmed.length <= 20;
909
+
910
+ if (isStructured) {
911
+ const structuredPattern = /(\d+)?([a-d])/g;
912
+ let match;
913
+ while ((match = structuredPattern.exec(trimmed)) !== null) {
914
+ const questionIndex = match[1] ? parseInt(match[1], 10) - 1 : 0;
915
+ const optionIndex = match[2].charCodeAt(0) - 97;
916
+
917
+ if (questionIndex >= 0 && questionIndex < questions.length) {
918
+ const q = questions[questionIndex];
919
+ if (optionIndex >= 0 && optionIndex < (q.options?.length || 0)) {
920
+ answers[q.category] = q.options[optionIndex];
921
+ summaryParts.push(`${q.category}: ${q.options[optionIndex]}`);
922
+ }
923
+ }
924
+ }
925
+ }
926
+
927
+ if (Object.keys(answers).length === 0 && trimmed.length > 0) {
928
+ const interpretedAnswers = interpretFreeTextAnswers(answersText, questions);
929
+
930
+ if (Object.keys(interpretedAnswers).length > 0) {
931
+ Object.assign(answers, interpretedAnswers);
932
+ for (const [category, value] of Object.entries(interpretedAnswers)) {
933
+ summaryParts.push(`${category}: ${value}`);
934
+ }
935
+ } else {
936
+ if (questions.length > 0) {
937
+ answers[questions[0].category] = answersText.trim();
938
+ summaryParts.push(`${questions[0].category}: ${answersText.trim()}`);
939
+ }
940
+ }
941
+ }
942
+
943
+ return {
944
+ answers,
945
+ summary: summaryParts.length > 0
946
+ ? `User clarified: ${summaryParts.join('; ')}`
947
+ : 'User provided unstructured answers to clarifying questions.',
948
+ };
949
+ }
950
+
951
+ function interpretFreeTextAnswers(text, questions) {
952
+ const answers = {};
953
+ const lowerText = text.toLowerCase().trim();
954
+
955
+ const shorthandMap = {
956
+ 'detailed': 'Very detailed (full analysis)',
957
+ 'brief': 'Brief (quick summary)',
958
+ 'quick': 'Brief (quick summary)',
959
+ 'summary': 'Brief (quick summary)',
960
+ 'comprehensive': 'Comprehensive overview',
961
+ 'overview': 'Comprehensive overview',
962
+ 'focused': 'Focused summary',
963
+ 'highlights': 'Key highlights only',
964
+ '2026': '2026 (current year)',
965
+ 'this year': '2026 (current year)',
966
+ '2025': '2025',
967
+ 'last year': '2025',
968
+ 'all': 'All types',
969
+ 'everything': 'All types',
970
+ 'skip': 'skip',
971
+ 'proceed': 'skip',
972
+ 'no': 'skip',
973
+ 'english': 'English',
974
+ 'thai': 'Thai',
975
+ 'chinese': 'Chinese',
976
+ };
977
+
978
+ for (const q of questions) {
979
+ if (!q.options || q.options.length === 0) continue;
980
+
981
+ for (const opt of q.options) {
982
+ if (lowerText === opt.toLowerCase()) {
983
+ answers[q.category] = opt;
984
+ break;
985
+ }
986
+ }
987
+
988
+ if (!answers[q.category]) {
989
+ for (const [key, value] of Object.entries(shorthandMap)) {
990
+ if (lowerText.includes(key) && q.options.includes(value)) {
991
+ answers[q.category] = value;
992
+ break;
993
+ }
994
+ }
995
+ }
996
+
997
+ if (!answers[q.category]) {
998
+ for (const opt of q.options) {
999
+ const optLower = opt.toLowerCase();
1000
+ if (optLower.includes(lowerText) || lowerText.includes(optLower.split(' ')[0])) {
1001
+ answers[q.category] = opt;
1002
+ break;
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ return answers;
1009
+ }
1010
+
1011
+ export async function emitClarifyingQuestions(questions, queryId, options = {}) {
1012
+ const { waitForResponse, timeoutMs = 30000 } = options;
1013
+
1014
+ eventBus.emit(Events.AGENT_CLARIFYING_QUESTIONS, {
1015
+ queryId,
1016
+ questions,
1017
+ timestamp: Date.now(),
1018
+ });
1019
+
1020
+ if (typeof waitForResponse === 'function') {
1021
+ try {
1022
+ const answers = await Promise.race([
1023
+ waitForResponse(queryId, questions),
1024
+ new Promise((_, reject) =>
1025
+ setTimeout(() => reject(new Error('Clarifying questions timed out')), timeoutMs)
1026
+ ),
1027
+ ]);
1028
+ return parseClarifyingAnswers(answers, questions);
1029
+ } catch {
1030
+ return { answers: {}, summary: 'Clarifying questions timed out; proceeding with original query.' };
1031
+ }
1032
+ }
1033
+
1034
+ return { answers: {}, summary: 'No response handler provided; proceeding with original query.' };
1035
+ }
1036
+
1037
+ export function buildClarificationContext(clarifyingResult, originalQuery) {
1038
+ if (!clarifyingResult || Object.keys(clarifyingResult.answers || {}).length === 0) {
1039
+ return '';
1040
+ }
1041
+
1042
+ const { answers, summary } = clarifyingResult;
1043
+
1044
+ const isDramaQuery = /(ซีรีส์|ละคร|drama|series|in blossom|ฮวาเจียนลิ่ง|花间令|花間令|หยุนซี|yunxi|นางพญางูขาว|드라마|ドラマ)/i.test(originalQuery);
1045
+ const isMusicQuery = /(เพลง|song|music|ost|album|single|find the way|曲|歌|음악|노래)/i.test(originalQuery);
1046
+
1047
+ const parts = [];
1048
+ parts.push('[SYSTEM CLARIFICATION]');
1049
+ parts.push(`Original query: ${originalQuery}`);
1050
+ parts.push(`Clarifications provided:`);
1051
+
1052
+ for (const [category, value] of Object.entries(answers)) {
1053
+ // If the query is strictly about a drama but clarification was marked as Music, skip the contradictory field
1054
+ if (category === 'media_type' && isDramaQuery && !isMusicQuery && String(value).toLowerCase().includes('music')) {
1055
+ continue;
1056
+ }
1057
+ if (category === 'media_type' && isMusicQuery && !isDramaQuery && String(value).toLowerCase().includes('drama')) {
1058
+ continue;
1059
+ }
1060
+ parts.push(`- ${category}: ${value}`);
1061
+ }
1062
+
1063
+ if (summary) {
1064
+ parts.push(`Summary: ${summary}`);
1065
+ }
1066
+
1067
+ return parts.join('\n');
1068
+ }
1069
+
1070
+ export function getAmbiguityPatterns() {
1071
+ return [...AMBIGUITY_PATTERNS];
1072
+ }
1073
+
1074
+ export default {
1075
+ stripSystemDirectives,
1076
+ detectAmbiguities,
1077
+ isQueryTooVague,
1078
+ normalizeLang,
1079
+ SUPPORTED_LANGUAGES,
1080
+ formatQuestionsForCLI,
1081
+ formatQuestionsForSSE,
1082
+ parseClarifyingAnswers,
1083
+ emitClarifyingQuestions,
1084
+ calculateConfidence,
1085
+ generateClarifyingQuestions,
1086
+ buildClarificationContext,
1087
+ getAmbiguityPatterns,
1088
+ AMBIGUITY_PATTERNS,
1089
+ };