kiku-agent 1.2.0 → 1.3.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 (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +528 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. package/tokens.css +33 -2
@@ -2,9 +2,6 @@
2
2
 
3
3
  import { eventBus, Events } from './eventBus.js';
4
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
5
  export const SUPPORTED_LANGUAGES = ['en', 'th', 'zh', 'ja', 'ko'];
9
6
 
10
7
  const LANGUAGE_ALIASES = {
@@ -23,8 +20,6 @@ const LANGUAGE_ALIASES = {
23
20
  'ko-kr': 'ko',
24
21
  };
25
22
 
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
23
  export function normalizeLang(lang, fallback = 'en') {
29
24
  if (!lang || typeof lang !== 'string') return fallback;
30
25
  const key = lang.trim().toLowerCase().replace('_', '-');
@@ -247,7 +242,6 @@ const AMBIGUITY_PATTERNS = [
247
242
  }
248
243
  ];
249
244
 
250
- // Resolve a pattern's question + options for a given language with English fallback.
251
245
  function localizePattern(pattern, lang) {
252
246
  const questions = pattern.questions || {};
253
247
  const options = pattern.options || {};
@@ -259,6 +253,15 @@ function localizePattern(pattern, lang) {
259
253
  };
260
254
  }
261
255
 
256
+ const KNOWN_WORKS_OR_CHARACTERS_REGEX = /(花间令|花間令|In Blossom|ฮวาเจียนลิ่ง|芸汐传|芸汐伝|Yunxi|หยุนซี|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|The Blooms at Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|杨采薇|上官芷|หยางไฉ่เวย|ซ่างกวนจื่อ|韩芸汐|หานหยุนซี|白素贞|ไป๋ซู่เจิน|傅容|ฟู่หรง|姜保宁|เจียงเป่าหนิง|韩菱纱|หานหลิงซา)/i;
257
+
258
+ const SPECIFICITY_PATTERNS = [
259
+ /\b(exactly|specifically|precisely|particular)\b/i,
260
+ /\b\d{4}\b/,
261
+ /["'"]/,
262
+ /\b(by|from|in|at|on)\b.*\b(Ju Jingyi|鞠婧祎|จวีจิ้งอี)\b/i,
263
+ ];
264
+
262
265
  export function calculateConfidence(query, detectedAmbiguities = []) {
263
266
  if (!query || typeof query !== 'string') {
264
267
  return { score: 0, level: 'very_low', shouldAsk: true };
@@ -267,8 +270,6 @@ export function calculateConfidence(query, detectedAmbiguities = []) {
267
270
  const trimmed = query.trim();
268
271
  let score = 1.0;
269
272
 
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
273
  const cjkChars = (trimmed.match(/[\u3040-\u30FF\u3400-\u9FFF\uAC00-\uD7AF]/g) || []).length;
273
274
  const wordCount = cjkChars > 0
274
275
  ? Math.max(1, Math.round(cjkChars / 2))
@@ -277,8 +278,7 @@ export function calculateConfidence(query, detectedAmbiguities = []) {
277
278
  else if (wordCount < 5) score -= 0.1;
278
279
  else if (wordCount > 15) score += 0.1;
279
280
 
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)) {
281
+ if (KNOWN_WORKS_OR_CHARACTERS_REGEX.test(trimmed)) {
282
282
  return {
283
283
  score: 0.95,
284
284
  level: 'high',
@@ -287,14 +287,7 @@ export function calculateConfidence(query, detectedAmbiguities = []) {
287
287
  };
288
288
  }
289
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) {
290
+ for (const pattern of SPECIFICITY_PATTERNS) {
298
291
  if (pattern.test(trimmed)) score += 0.15;
299
292
  }
300
293
 
@@ -346,8 +339,6 @@ export function detectAmbiguities(query, detectedLang = 'en') {
346
339
  return detected;
347
340
  }
348
341
 
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
342
  const KNOWN_ENTITY_PATTERNS = [
352
343
  {
353
344
  key: 'in_blossom',
@@ -392,12 +383,22 @@ export function stripSystemDirectives(text) {
392
383
 
393
384
  const KNOWN_ENTITY_REGEX = /(花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง|芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่|新白娘子传奇|White Snake|นางพญางูขาว|如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ|SNH48)/i;
394
385
 
395
- // Returns a recognized topic label (localized) that appears in the query, or null.
386
+ const ENTITY_REGEX_MAP = {
387
+ in_blossom: /(?:in_blossom|花间令|花間令|In Blossom|อินบลอสซัม|ฮวาเจียนหลิง)/i,
388
+ legend_of_yunxi: /(?:legend_of_yunxi|芸汐传|芸汐伝|Yunxi|ตำนานหยุนซี|หยุนซี)/i,
389
+ sword_fairy_4: /(?:sword_fairy_4|仙剑四|仙劍四|Sword and Fairy|เซียนกระบี่)/i,
390
+ white_snake: /(?:white_snake|新白娘子传奇|White Snake|นางพญางูขาว)/i,
391
+ ruyi_pavilion: /(?:ruyi_pavilion|如意芳霏|Ruyi Pavilion|กรุ่นรักกลิ่นบุปผา)/i,
392
+ rebirth_for_you: /(?:rebirth_for_you|嘉南传|嘉南傳|Rebirth For You|มู่หนานจือ)/i,
393
+ snh48: /(?:snh48|SNH48)/i,
394
+ };
395
+
396
396
  function extractQueryTopic(query, lang) {
397
397
  if (!query || typeof query !== 'string') return null;
398
398
  const clean = stripSystemDirectives(query);
399
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; }
400
+ const rx = ENTITY_REGEX_MAP[p.key];
401
+ return rx ? rx.test(clean) : false;
401
402
  });
402
403
  if (hit) return (hit.labels && hit.labels[lang]) || hit.labels.en;
403
404
  const rawMatch = clean.match(KNOWN_ENTITY_REGEX);
@@ -422,7 +423,6 @@ function escapeRegex(str) {
422
423
  return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
423
424
  }
424
425
 
425
- // Extract a short noun phrase that likely represents the topic (fallback when no known entity).
426
426
  function extractQueryPhrase(query) {
427
427
  if (!query || typeof query !== 'string') return '';
428
428
  const cleaned = stripSystemDirectives(query)
@@ -447,8 +447,6 @@ function synthesizeDynamicQuestions(query, lang = 'en', detectedAmbiguities = []
447
447
 
448
448
  const topic = extractQueryTopic(query, lang) || extractQueryPhrase(query) || null;
449
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
450
  if (topic) {
453
451
  const subject = String(topic).trim();
454
452
  if (!addedCategories.has('subject_focus') && results.length < maxQuestions) {
@@ -524,154 +522,219 @@ function synthesizeDynamicQuestions(query, lang = 'en', detectedAmbiguities = []
524
522
  }
525
523
  }
526
524
 
527
- // 2) Specific detected ambiguity categories (from the query).
525
+ const clean = stripSystemDirectives(query);
526
+ const isMusic = /(เพลง|ร้องเพลง|ดนตรี|ซิงเกิล|อัลบั้ม|สเตจ|คอนเสิร์ต|ost|song|music|single|album|concert|stage|find the way|0\.2s|曲|歌|음악|노래)/i.test(clean);
527
+ const isDrama = /(ซีรีส์|ละคร|หนัง|ภาพยนตร์|การแสดง|บทบาท|ตัวละคร|เรื่อง|ฮวาเจียน|หยุนซี|งูขาว|仙剑|花间令|芸汐|drama|series|movie|acting|character|role|in blossom|yunxi)/i.test(clean);
528
+ const isFashion = /(แบรนด์|พรีเซนเตอร์|แอมบาสเดอร์|แฟชั่น|เครื่องประดับ|ชุด|แต่งหน้า|เมคอัพ|fashion|brand|ambassador|fred|versace|prsr|makeup)/i.test(clean);
529
+
528
530
  if (Array.isArray(detectedAmbiguities) && detectedAmbiguities.length > 0) {
529
531
  for (const amb of detectedAmbiguities) {
530
532
  if (amb.category && amb.question && amb.options && !addedCategories.has(amb.category)) {
533
+
534
+ if (amb.category === 'media_type' && ((isMusic && !isDrama) || (isDrama && !isMusic))) {
535
+ continue;
536
+ }
531
537
  addQ(amb.category, amb.question, (amb.options || []).slice(0, 4));
532
538
  }
533
539
  }
534
540
  }
535
541
 
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
- 'กระแสตอบรับและรางวัล'
542
+ if (isMusic && !isDrama) {
543
+ if (lang === 'th') {
544
+ addQ('music_aspect', 'สนใจผลงานเพลงแนวไหนเป็นพิเศษคะ?', [
545
+ 'เพลงประกอบซีรีส์ (OST) ซึ้งๆ',
546
+ 'ซิงเกิลเดี่ยวแนวป๊อป/แดนซ์',
547
+ 'สเตจแสดงสดไวรัล',
548
+ 'ผลงานเพลงทั้งหมด'
549
+ ]);
550
+ addQ('stage_vibe', 'อยากเจาะลึกเพลงฮิตช่วงไหนคะ?', [
551
+ 'ซิงเกิลใหม่ล่าสุด (0.2s, FIND THE WAY)',
552
+ 'เพลงคลาสสิกสร้างชื่อ (叹云兮, 恋爱告急)',
553
+ 'เพลงที่มีสเตจเต้นไวรัล'
554
+ ]);
555
+ } else if (lang === 'zh') {
556
+ addQ('music_aspect', '想了解哪种风格的音乐作品?', [
557
+ '经典影视原声 (OST)',
558
+ '个人 Solo 唱跳单曲',
559
+ '出圈神级现场舞台',
560
+ '全部音乐作品'
561
+ ]);
562
+ addQ('stage_vibe', '想聚焦哪个时期的音乐?', [
563
+ '最新单曲 (0.2s, FIND THE WAY)',
564
+ '经典代表作 (叹云兮, 恋爱告急)',
565
+ '出圈舞台曲目'
566
+ ]);
567
+ } else if (lang === 'ja') {
568
+ addQ('music_aspect', 'どのスタイルの音楽作品に興味がありますか?', [
569
+ 'ドラマ主題歌 (OST)',
570
+ 'ソロ楽曲・ダンス曲',
571
+ '話題のライブステージ',
572
+ '全楽曲'
573
+ ]);
574
+ addQ('stage_vibe', '特に注目したい楽曲の時期はありますか?', [
575
+ '最新シングル (0.2s, FIND THE WAY)',
576
+ '定番の代表曲 (叹云兮, 恋爱告急)',
577
+ 'バイラルステージ曲'
578
+ ]);
579
+ } else if (lang === 'ko') {
580
+ addQ('music_aspect', '어떤 스타일의 음악 작품에 더 관심이 있으신가요?', [
581
+ '드라마 OST',
582
+ '솔로 팝·댄스 곡',
583
+ '화제의 라이브 무대',
584
+ '전체 음악'
585
+ ]);
586
+ addQ('stage_vibe', '특히 주목하고 싶은 시기의 곡이 있으신가요?', [
587
+ '최신 싱글 (0.2s, FIND THE WAY)',
588
+ '대표 명곡 (탄윈시, 연애고급)',
589
+ '인기 무대 곡'
590
+ ]);
591
+ } else {
592
+ addQ('music_aspect', 'Which musical style are you most interested in?', [
593
+ 'Iconic Drama OSTs',
594
+ 'Solo Pop & Dance Singles',
595
+ 'Viral Live Stages',
596
+ 'Complete Discography'
563
597
  ]);
564
598
  }
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
- '故事与人设解读',
599
+ } else if (isDrama && !isMusic) {
600
+ if (lang === 'th') {
601
+ addQ('drama_genre', 'สนใจซีรีส์สไตล์ไหนเป็นพิเศษคะ?', [
602
+ 'พีเรียดสืบสวนเข้มข้น (ฮวาเจียนลิ่ง)',
603
+ 'โรแมนติกแฟนตาซี/เทพเซียน',
604
+ 'ผลงานล่าสุดที่กำลังจะมา (Veil of Shadows)',
605
+ 'ซีรีส์มาสเตอร์พีซทั้งหมด'
606
+ ]);
607
+ addQ('drama_aspect', 'อยากเน้นเนื้อหาด้านใดในซีรีส์คะ?', [
608
+ 'บทบาทและการแสดงอันเป็นเอกลักษณ์',
609
+ 'เคมีคู่และเส้นเรื่องความรัก',
610
+ 'กระแสตอบรับ เรตติ้ง และรางวัล'
611
+ ]);
612
+ } else if (lang === 'zh') {
613
+ addQ('drama_genre', '想探索哪种类型的影视剧作?', [
614
+ '古装悬疑探案 (如《花间令》)',
615
+ '仙侠奇幻/浪漫言情',
616
+ '即将上线的待播新剧',
617
+ '历年代表作全景'
618
+ ]);
619
+ addQ('drama_aspect', '想重点了解影视作品的哪一方面?', [
620
+ '角色塑造与演技高光',
621
+ 'CP默契与感情线',
590
622
  '热度口碑与奖项'
591
623
  ]);
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
- '評価と賞'
624
+ } else if (lang === 'ja') {
625
+ addQ('drama_genre', 'どのジャンルのドラマに興味がありますか?', [
626
+ '時代劇・サスペンス探偵 (花間令など)',
627
+ '仙侠・ロマンスファンタジー',
628
+ '公開予定の最新作',
629
+ '歴代の代表作'
630
+ ]);
631
+ addQ('drama_aspect', 'ドラマのどの側面に注目しますか?', [
632
+ '演技と独自のキャラクター',
633
+ '相手役とのケミ・恋愛模様',
634
+ '反響・視聴率・受賞歴'
635
+ ]);
636
+ } else if (lang === 'ko') {
637
+ addQ('drama_genre', '어떤 장르의 드라마를 더 알아보고 싶으신가요?', [
638
+ '고장극·미스터리 수사 (화간령 등)',
639
+ '선협·로맨스 판타지',
640
+ '공개 예정 신작',
641
+ '역대 대표작'
642
+ ]);
643
+ addQ('drama_aspect', '드라마의 어떤 점에 주목하고 싶으신가요?', [
644
+ '연기와 매력적인 캐릭터',
645
+ '주연 케미와 로맨스',
646
+ '반응·시청률·수상 내역'
647
+ ]);
648
+ } else {
649
+ addQ('drama_genre', 'Which drama style would you like to explore?', [
650
+ 'Historical Mystery & Investigation',
651
+ 'Romance & Xianxia Fantasy',
652
+ 'Upcoming & Latest Projects',
653
+ 'All Masterpiece Dramas'
619
654
  ]);
620
655
  }
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
- '평가와 수상'
656
+ } else if (isFashion) {
657
+ if (lang === 'th') {
658
+ addQ('fashion_aspect', 'สนใจเจาะลึกด้านแฟชั่นและลุคไวรัลมุมไหนคะ?', [
659
+ 'เครื่องประดับชั้นสูง FRED Paris',
660
+ 'แบรนด์แอมบาสเดอร์ระดับโลก (Versace, PRSR)',
661
+ 'สไตล์การแต่งหน้า Ju Jingyi Makeup ยอดฮิต'
662
+ ]);
663
+ } else if (lang === 'zh') {
664
+ addQ('fashion_aspect', '想了解哪方面的时尚与出圈造型?', [
665
+ '法国高级珠宝 FRED Paris',
666
+ '全球品牌代言 (Versace, PRSR)',
667
+ '出圈鞠式美学与妆容'
668
+ ]);
669
+ } else if (lang === 'ja') {
670
+ addQ('fashion_aspect', 'どのファッションやビジュアルに注目しますか?', [
671
+ 'フランス高級ジュエリー FRED Paris',
672
+ 'グローバルブランドアンバサダー (Versace, PRSR)',
673
+ '話題のキク風メイク&スタイル'
674
+ ]);
675
+ } else if (lang === 'ko') {
676
+ addQ('fashion_aspect', '어떤 패션과 스타일링에 관심이 있으신가요?', [
677
+ '프랑스 하이주얼리 FRED Paris',
678
+ '글로벌 브랜드 앰버서더 (Versace, PRSR)',
679
+ '화제의 쥐징이 메이크업 & 스타일'
680
+ ]);
681
+ } else {
682
+ addQ('fashion_aspect', 'Which fashion aspect would you like to explore?', [
683
+ 'FRED Paris High Jewelry',
684
+ 'Global Ambassadorships (Versace, PRSR)',
685
+ 'Viral Ju Jingyi Makeup & Style'
647
686
  ]);
648
687
  }
649
688
  } 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'
689
+ if (lang === 'th') {
690
+ addQ('fan_focus', 'อยากให้เสี่ยวจวีพาไปสำรวจมิติไหนก่อนดีคะ?', [
691
+ 'ผลงานมาสเตอร์พีซและบทบาทสร้างชื่อ',
692
+ 'อัปเดตผลงานและกิจกรรมล่าสุด (2024-2026)',
693
+ 'เสน่ห์ด้านดนตรีและสเตจแสดงสด'
694
+ ]);
695
+ addQ('timeframe_focus', 'ช่วงเวลาที่อยากให้โฟกัสเป็นพิเศษ?', [
696
+ 'ผลงานยุคศิลปินเดี่ยวปัจจุบัน',
697
+ 'ผลงานสร้างชื่อคลาสสิกในอดีต',
698
+ 'ไทม์ไลน์พัฒนาการทั้งหมด'
699
+ ]);
700
+ } else if (lang === 'zh') {
701
+ addQ('fan_focus', '想从小鞠的哪个精彩视角开始了解呢?', [
702
+ '出圈代表作与封神角色',
703
+ '近年最新动态与作品 (2024-2026)',
704
+ '舞台魅力与音乐全纪录'
705
+ ]);
706
+ addQ('timeframe_focus', '想聚焦哪个成长阶段呢?', [
707
+ '个人 Solo 发展期',
708
+ '经典作品巅峰期',
709
+ '全时段全景历程'
710
+ ]);
711
+ } else if (lang === 'ja') {
712
+ addQ('fan_focus', 'キクのどの魅力的なポイントから見ていきましょうか?', [
713
+ '代表作と名キャラクター',
714
+ '最新の活動・作品 (2024-2026)',
715
+ '音楽とステージパフォーマンス'
716
+ ]);
717
+ addQ('timeframe_focus', '特に注目したい活動時期はありますか?', [
718
+ '現在のソロ活動期',
719
+ '過去の名作時代',
720
+ '全活動期間'
721
+ ]);
722
+ } else if (lang === 'ko') {
723
+ addQ('fan_focus', '쥐징이의 어떤 매력적인 부분부터 살펴볼까요?', [
724
+ '대표작과 인기 캐릭터',
725
+ '최신 활동 및 작품 (2024-2026)',
726
+ '음악과 무대 퍼포먼스'
727
+ ]);
728
+ addQ('timeframe_focus', '특별히 주목하고 싶은 활동 시기가 있으신가요?', [
729
+ '현재 솔로 활동기',
730
+ '과거 대표작 시절',
731
+ '전체 활동 기간'
732
+ ]);
733
+ } else {
734
+ addQ('fan_focus', 'Which dimension would you like to explore first?', [
735
+ 'Signature Masterpieces & Breakthrough Roles',
736
+ 'Latest Updates & New Releases (2024-2026)',
737
+ 'Music & Stage Highlights'
675
738
  ]);
676
739
  }
677
740
  }
@@ -679,9 +742,18 @@ function synthesizeDynamicQuestions(query, lang = 'en', detectedAmbiguities = []
679
742
  return results.slice(0, maxQuestions);
680
743
  }
681
744
 
745
+ export function cleanThinkingBlocks(raw) {
746
+ if (!raw || typeof raw !== 'string') return '';
747
+ return raw
748
+ .replace(/<think>[\s\S]*?<\/think>/gi, '')
749
+ .replace(/<thought>[\s\S]*?<\/thought>/gi, '')
750
+ .replace(/```thought[\s\S]*?```/gi, '')
751
+ .trim();
752
+ }
753
+
682
754
  function extractJsonArray(text) {
683
755
  if (!text || typeof text !== 'string') return null;
684
- let cleaned = text.trim();
756
+ let cleaned = cleanThinkingBlocks(text);
685
757
  const fenceMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
686
758
  if (fenceMatch) {
687
759
  cleaned = fenceMatch[1].trim();
@@ -710,41 +782,45 @@ function extractJsonArray(text) {
710
782
 
711
783
  const LLM_SYSTEM_PROMPTS = {
712
784
  th:
713
- 'คุณคือ Kiku AI ระบบผู้ช่วยอัจฉริยะทางการของ "จวีจิ้งอี" (Ju Jingyi / 鞠婧祎 / เสี่ยงจวี)\n' +
714
- 'หน้าที่: วิเคราะห์คำถามของผู้ใช้ และคิดคำถามเจาะจง 3-4 ข้ออย่างเป็นมิตร สุภาพ (ลงท้ายด้วย คะ/นะคะ) เพื่อถามกลับผู้ใช้ก่อนจะเริ่มค้นหาข้อมูลหรือเริ่มกระบวนการ Timeline Agent\n' +
785
+ 'คุณคือ Kiku AI ระบบผู้ช่วยอัจฉริยะทางการและเพื่อนรู้ใจของแฟนคลับ "จวีจิ้งอี" (Ju Jingyi / 鞠婧祎 / เสี่ยวจวี)\n' +
786
+ 'หน้าที่: วิเคราะห์คำถามของแฟนคลับ และคิดคำถามชวนคุย/เจาะลึก 2-3 ข้ออย่างเป็นกันเอง สุภาพ น่ารัก (ลงท้ายด้วย คะ/นะคะ) เพื่อให้แฟนคลับเลือกมุมมองที่อยากรู้จริงก่อนเริ่มค้นหาข้อมูล\n' +
715
787
  'เป้าหมาย:\n' +
716
- '1. เจาะลึกความต้องการให้แคบลงและตรงกับสิ่งที่ผู้ใช้ถามจริง (เช่น บทบาท/ตัวละคร, ผลงานซีรีส์ vs เพลง, ช่วงปี พ.ศ./ค.ศ., ยุคปัจจุบัน vs ไอดอล SNH48, ความละเอียดของข้อมูล)\n' +
717
- '2. แต่ละข้อต้องให้ตัวเลือกคำตอบ 2-4 ตัวเลือกที่เกี่ยวข้องโดยตรงเพื่อให้ผู้ใช้กดเลือกได้ทันที\n' +
718
- '3. ห้ามพูดนอกเรื่อง หรือแต่งประโยคระบบคำสั่งลงในคำถาม\n' +
788
+ '1. วิเคราะห์สิ่งที่แฟนคลับถามจริงๆ แล้วสร้างคำถามเจาะลึกที่สร้างสรรค์ ตรงประเด็น และเข้าถึงใจแฟนคลับ เช่น หากถามถึงซีรีส์ ให้ชวนคุยมิติของตัวละคร/เคมีคู่/เบื้องหลังการแสดง หรือหากถามถึงเพลง ให้ชวนคุยสไตล์เพลง/ความหมาย/สเตจไวรัล\n' +
789
+ '2. หลีกเลี่ยงการถามคำถามแบบฟอร์มแห้งๆ ทั่วไป (ห้ามถามซ้ำซากเรื่อง ละครหรือเพลง, SNH48 หรือเดี่ยว, ปีไหน, ละเอียดแค่ไหน หากบริบทคำถามเดิมชัดเจนอยู่แล้ว)\n' +
790
+ '3. แต่ละข้อให้ตัวเลือกคำตอบ 2-4 ตัวเลือกที่สั้นกระชับ ชัดเจน น่ากดเลือก\n' +
719
791
  '4. ส่งคำตอบเป็น JSON Array เท่านั้น ห้ามใส่คำอธิบายอื่นนอกเหนือจาก JSON:\n' +
720
- '[{"category": "subject_focus", "question": "คำถามภาษาไทย", "options": ["ตัวเลือก 1", "ตัวเลือก 2", "ตัวเลือก 3"]}]',
792
+ '[{"category": "drama_aspect", "question": "เกี่ยวกับฮวาเจียนลิ่ง สนใจเจาะลึกมิติตัวละครหรือเคมีคู่กับพานเยว่คะ?", "options": ["มิติตัวละครหยางไฉ่เวย", "เคมีคู่พานเยว่", "เบื้องหลังการถ่ายทำ"]}]',
721
793
  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"]}]',
794
+ '你是鞠婧祎官方智能粉丝助手 Kiku,也是粉丝们的贴心同好小伙伴。\n' +
795
+ '任务:根据粉丝的具体提问,主动构思 2-3 个亲切有爱、针对性极强的追问,让粉丝选择最想探索的专属视角。\n' +
796
+ '要求:\n' +
797
+ '1. 紧密结合问题核心(如角色内心、名场面解析、声乐/舞台魅力、造型出圈点、幕后花絮),切忌千篇一律的固定模板问卷(切勿生硬追问影视还是音乐、SNH还是Solo等已明确的信息)。\n' +
798
+ '2. 每个追问提供 2-4 个精炼、有吸引力的点击选项。\n' +
799
+ '3. 仅输出合法的 JSON Array 格式,不要包含任何多余文字:\n' +
800
+ '[{"category": "highlight_aspect", "question": "追问内容?", "options": ["选项1", "选项2", "选项3"]}]',
801
+ en:
802
+ 'You are Kiku AI, the official intelligent assistant and companion for Ju Jingyi (鞠婧祎) fans.\n' +
803
+ 'Task: Analyze the fan\'s inquiry and generate 2-3 engaging, creative, and highly targeted follow-up questions tailored specifically to what they asked, allowing them to explore deeper nuances.\n' +
804
+ 'Guidelines:\n' +
805
+ '1. Craft personalized exploration angles (e.g., character psychology, signature scenes, vocal style/stage presence, fashion highlights, behind-the-scenes) instead of dry, robotic survey templates.\n' +
806
+ '2. Never ask redundant boilerplate questions (like "drama vs music" or "solo vs SNH48" if the context is already implied).\n' +
807
+ '3. Provide 2-4 concise, clickable options per question.\n' +
808
+ '4. Output ONLY a valid JSON Array with no other text:\n' +
809
+ '[{"category": "fan_focus", "question": "Question?", "options": ["Option 1", "Option 2", "Option 3"]}]',
727
810
  ja:
728
811
  'あなたは鞠婧祎(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"]}]',
812
+ 'タスク:ファンの質問を分析し、より深く魅力的な回答を届けるために、文脈に合わせた2〜3件の親しみやすく的確な確認質問を日本語で生成してください。\n' +
813
+ '固定的なアンケート調(毎回ドラマか音楽か、時期はいつか等)を避け、役柄の魅力、名シーン、楽曲の世界観などファンがワクワクする切り口を提示してください。\n' +
814
+ '各質問には2〜4個の簡潔な選択肢を付けて、JSON配列のみを出力してください:\n' +
815
+ '[{"category": "fan_focus", "question": "質問内容?", "options": ["選択肢1", "選択肢2", "選択肢3"]}]',
733
816
  ko:
734
817
  '당신은 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"]}]',
818
+ '임무: 팬의 질문을 분석하여 더 깊이 있고 매력적인 답변을 제공할 수 있도록, 문맥에 맞춘 2-3개의 다정하고 흥미로운 탐색 질문을 한국어로 생성하세요.\n' +
819
+ '기계적인 정형화된 설문(매번 드라마vs음악, 시기 등)을 지양하고, 캐릭터 매력, 명장면, 음악 분위기 등 팬 맞춤형 포인트를 제시하세요.\n' +
820
+ '각 질문에는 2-4개의 간결한 선택지를 제공하고, JSON 배열만 출력하세요:\n' +
821
+ '[{"category": "fan_focus", "question": "질문 내용?", "options": ["선택지1", "선택지2", "선택지3"]}]'
745
822
  };
746
823
 
747
- // Shown when a model-generated question arrives without usable options.
748
824
  const FALLBACK_OPTIONS = {
749
825
  en: ['Yes', 'No', 'All'],
750
826
  th: ['ใช่', 'ไม่ใช่', 'ทั้งหมด'],
@@ -765,11 +841,8 @@ export async function generateClarifyingQuestions(query, lang = 'en', options =
765
841
  .filter(line => !line.startsWith('system:'))
766
842
  .join('\n');
767
843
 
768
- const ambiguityAreas = detectedAmbiguities.map(a => a.category).join(', ');
769
-
770
844
  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:`;
845
+ const userPrompt = `User inquiry: "${cleanQuery}"\n${recentHistory ? `Recent conversation context:\n${recentHistory}\n` : ''}Formulate 2-3 creative, engaging, fan-centric follow-up questions tailored specifically to this inquiry as a JSON array:`;
773
846
 
774
847
  try {
775
848
  const controller = new AbortController();
@@ -795,7 +868,7 @@ export async function generateClarifyingQuestions(query, lang = 'en', options =
795
868
  const validated = rawQuestions
796
869
  .filter(q => q && typeof q.question === 'string' && q.question.trim().length > 0)
797
870
  .map((q, idx) => ({
798
- category: String(q.category || `clarify_${idx + 1}`).toLowerCase().replace(/[^a-z0-9_]/g, '_'),
871
+ category: String(q.category || `fan_focus_${idx + 1}`).toLowerCase().replace(/[^a-z0-9_]/g, '_'),
799
872
  question: q.question.trim(),
800
873
  options: Array.isArray(q.options) && q.options.length > 0
801
874
  ? q.options.map(opt => String(opt).trim()).filter(Boolean).slice(0, 4)
@@ -807,7 +880,6 @@ export async function generateClarifyingQuestions(query, lang = 'en', options =
807
880
  }
808
881
  }
809
882
  } catch {
810
- // Fast fallback to dynamic contextual synthesis if model call fails/times out
811
883
  }
812
884
  }
813
885
 
@@ -821,7 +893,6 @@ export function isQueryTooVague(query) {
821
893
 
822
894
  if (trimmed.length < 3) return true;
823
895
 
824
- // Punctuation/symbols only — treat as vague (includes CJK punctuation).
825
896
  if (/^[^\w\u0E00-\u0E7F\u3040-\u30FF\u3400-\u9FFF\uAC00-\uD7AF]+$/.test(trimmed)) return true;
826
897
 
827
898
  const words = trimmed.split(/\s+/);
@@ -882,7 +953,6 @@ export function formatQuestionsForSSE(questions, lang = 'en') {
882
953
  };
883
954
  }
884
955
 
885
- // Words that mean "skip these questions" across the supported languages.
886
956
  const SKIP_WORDS = new Set([
887
957
  'skip', 'next', 'no', 'none', 'pass', 'proceed',
888
958
  'ข้าม', 'ไม่', 'ไม่ต้อง', 'ไม่ต้องถาม', 'ผ่าน',
@@ -948,33 +1018,33 @@ export function parseClarifyingAnswers(answersText, questions) {
948
1018
  };
949
1019
  }
950
1020
 
1021
+ const ANSWER_SHORTHAND_MAP = Object.freeze({
1022
+ 'detailed': 'Very detailed (full analysis)',
1023
+ 'brief': 'Brief (quick summary)',
1024
+ 'quick': 'Brief (quick summary)',
1025
+ 'summary': 'Brief (quick summary)',
1026
+ 'comprehensive': 'Comprehensive overview',
1027
+ 'overview': 'Comprehensive overview',
1028
+ 'focused': 'Focused summary',
1029
+ 'highlights': 'Key highlights only',
1030
+ '2026': '2026 (current year)',
1031
+ 'this year': '2026 (current year)',
1032
+ '2025': '2025',
1033
+ 'last year': '2025',
1034
+ 'all': 'All types',
1035
+ 'everything': 'All types',
1036
+ 'skip': 'skip',
1037
+ 'proceed': 'skip',
1038
+ 'no': 'skip',
1039
+ 'english': 'English',
1040
+ 'thai': 'Thai',
1041
+ 'chinese': 'Chinese',
1042
+ });
1043
+
951
1044
  function interpretFreeTextAnswers(text, questions) {
952
1045
  const answers = {};
953
1046
  const lowerText = text.toLowerCase().trim();
954
1047
 
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
1048
  for (const q of questions) {
979
1049
  if (!q.options || q.options.length === 0) continue;
980
1050
 
@@ -986,7 +1056,7 @@ function interpretFreeTextAnswers(text, questions) {
986
1056
  }
987
1057
 
988
1058
  if (!answers[q.category]) {
989
- for (const [key, value] of Object.entries(shorthandMap)) {
1059
+ for (const [key, value] of Object.entries(ANSWER_SHORTHAND_MAP)) {
990
1060
  if (lowerText.includes(key) && q.options.includes(value)) {
991
1061
  answers[q.category] = value;
992
1062
  break;
@@ -1050,7 +1120,6 @@ export function buildClarificationContext(clarifyingResult, originalQuery) {
1050
1120
  parts.push(`Clarifications provided:`);
1051
1121
 
1052
1122
  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
1123
  if (category === 'media_type' && isDramaQuery && !isMusicQuery && String(value).toLowerCase().includes('music')) {
1055
1124
  continue;
1056
1125
  }
@@ -1082,6 +1151,7 @@ export default {
1082
1151
  parseClarifyingAnswers,
1083
1152
  emitClarifyingQuestions,
1084
1153
  calculateConfidence,
1154
+ cleanThinkingBlocks,
1085
1155
  generateClarifyingQuestions,
1086
1156
  buildClarificationContext,
1087
1157
  getAmbiguityPatterns,