kiku-agent 1.3.2 → 1.3.4

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 (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -80,6 +80,8 @@ export function extractAgentFollowUpQuestions(text) {
80
80
  }
81
81
 
82
82
  let cleanText = text;
83
+ cleanText = cleanText.replace(/(?:\[TOOLCALL\]|<tool_call>|<toolcall>|<\|tool_call\|>)\s*(?:call:)?[a-zA-Z0-9_]+\s*\{[\s\S]*?\}\s*(?:<toolcall\|?>|<\/tool_call>|<\/toolcall>|\[\/TOOLCALL\]|$)/gi, '');
84
+ cleanText = cleanText.replace(/<\|?toolcall\|?>|<\/?tool_call>|<\/?toolcall>|\[\/?TOOLCALL\]|\[TOOL_CALLS\]|<\|["']\|>/gi, '').trim();
83
85
  const suggestions = [];
84
86
 
85
87
 
@@ -146,114 +148,766 @@ export function extractAgentFollowUpQuestions(text) {
146
148
  return { cleanText, suggestions: dedupeSuggestions(suggestions, new Set()) };
147
149
  }
148
150
 
149
- export function generateDynamicFollowUps({ query = '', answer = '', tools = [], recentKeys = null } = {}) {
151
+ const KIKU_SIGNAL = /จวีจิ้งอี|ju\s*jingyi|jujingyi|鞠婧祎|蜜橘|\bmiju\b|เสี่ยวจวี|\bkiku\b|snh48|stars\s*dream/i;
152
+ const KIKU_WORKS_SIGNAL = /(?:ฮวาเจียน|in blossom|花间令|หยางไฉ่เวย|ซ่างกวนจื่อ|หยุนซี|yunxi|芸汐|หานหยุนซี|ไป๋ซู่เจิน|white snake|นางพญางูขาว|ลู่หวู่อี้|veil of shadows|月鳞绮纪|千香|พันกลิ่นบุปผา|eternal fragrance|仙剑四|sword and fairy|เซียนกระบี่|花戎|ฮวาหรง|beauty of resilience|如意芳霏|ruyi|ฟู่หรง|嘉南传|rebirth for you|มู่หนานจือ)/i;
153
+
154
+ const CATALOG_EVIDENCE_TOOLS = new Set([
155
+ 'search_works', 'get_work_detail', 'get_featured_works', 'search_discography',
156
+ 'get_song_detail', 'get_character_roles', 'get_awards', 'get_profile_and_fandom',
157
+ 'get_quotes', 'get_music_videos', 'get_media_links', 'get_career_timeline',
158
+ 'get_social_feed', 'get_gallery_photos', 'get_brand_endorsements',
159
+ 'get_entertainment_metrics', 'get_industry_index', 'search_bilibili',
160
+ 'search_youtube', 'get_youtube_transcript', 'search_music_platforms',
161
+ 'verify_claim', 'run_subagent', 'run_deep_research', 'suggest_topics'
162
+ ]);
163
+
164
+ function hasCatalogEvidence(tools) {
165
+ return (Array.isArray(tools) ? tools : []).some(t =>
166
+ t && CATALOG_EVIDENCE_TOOLS.has(t.name) && t.result &&
167
+ t.result.status !== 'error' && t.result.status !== 'not_found' && t.result.status !== 'no_results');
168
+ }
169
+
170
+ export function isOffTopicChat(combinedText, tools) {
171
+ const str = String(combinedText || '');
172
+ if (KIKU_SIGNAL.test(str)) return false;
173
+ if (KIKU_WORKS_SIGNAL.test(str)) return false;
174
+ if (hasCatalogEvidence(tools)) return false;
175
+ return true;
176
+ }
177
+
178
+ function detectFollowUpLang(text, explicitLang) {
179
+ if (explicitLang && typeof explicitLang === 'string') {
180
+ const l = explicitLang.toLowerCase().slice(0, 2);
181
+ if (['th', 'zh', 'en', 'ja', 'ko'].includes(l)) return l;
182
+ }
183
+ const str = String(text || '');
184
+ const thCount = (str.match(/[\u0E00-\u0E7F]/g) || []).length;
185
+ const zhCount = (str.match(/[\u4E00-\u9FFF]/g) || []).length;
186
+ const jaCount = (str.match(/[\u3040-\u30FF]/g) || []).length;
187
+ const koCount = (str.match(/[\uAC00-\uD7AF]/g) || []).length;
188
+
189
+ if (thCount > 0 && thCount >= zhCount) return 'th';
190
+ if (zhCount > thCount) return 'zh';
191
+ if (jaCount > 0) return 'ja';
192
+ if (koCount > 0) return 'ko';
193
+ if (thCount > 0) return 'th';
194
+ return 'en';
195
+ }
196
+
197
+ function detectSpecializedOpenDomainFollowUps(query, lang = 'th') {
198
+ const q = String(query || '').toLowerCase().trim();
199
+ if (!q) return null;
200
+
201
+ if (/\\lim|ลิมิต|\blimit\b|\b0\/0\b/.test(q)) {
202
+ if (lang === 'zh') {
203
+ return [
204
+ { label: '📐 洛必达法则 (L\'Hôpital) 应用技巧', query: '如何使用洛必达法则求解 0/0 型未定式极限?' },
205
+ { label: '🔍 因式分解与有理化方法', query: '求极限时的因式分解与分子分母有理化技巧' },
206
+ { label: '📊 左右极限与函数连续性判定', query: '如何判断左右极限以及函数在某点处的连续性?' },
207
+ { label: '✏️ 极限综合练习题与解析', query: '提供更多微积分求极限的经典练习题与详细解析' }
208
+ ];
209
+ }
210
+ if (lang === 'en') {
211
+ return [
212
+ { label: '📐 How to apply L\'Hôpital\'s Rule', query: 'How to apply L\'Hôpital\'s Rule to limits with indeterminate form 0/0?' },
213
+ { label: '🔍 Factoring and Conjugate techniques', query: 'Explain factoring and conjugate techniques for solving limit problems' },
214
+ { label: '📊 Continuity and One-sided limits', query: 'How to determine function continuity and evaluate one-sided limits?' },
215
+ { label: '✏️ More practice limit problems', query: 'Provide more calculus limit practice problems with step-by-step solutions' }
216
+ ];
217
+ }
218
+ return [
219
+ { label: '📐 วิธีใช้กฎของโลปิตาล (L\'Hôpital)', query: 'แนะนำวิธีใช้กฎของโลปิตาล (L\'Hôpital\'s Rule) กับโจทย์ลิมิตในรูปแบบ 0/0' },
220
+ { label: '🔍 เทคนิคการแยกตัวประกอบและคูณสังยุค', query: 'เทคนิคการแยกตัวประกอบและคูณสังยุค (Conjugate) เพื่อหาค่าลิมิต' },
221
+ { label: '📊 การตรวจสอบความต่อเนื่องของฟังก์ชัน', query: 'วิธีตรวจสอบลิมิตซ้าย-ขวาและความต่อเนื่องของฟังก์ชัน' },
222
+ { label: '✏️ ตัวอย่างโจทย์ลิมิตเพิ่มเติมพร้อมเฉลย', query: 'ขอตัวอย่างโจทย์ลิมิตแคลคูลัสในรูปแบบ 0/0 เพิ่มเติมพร้อมเฉลยวิธีทำ' }
223
+ ];
224
+ }
225
+
226
+ if (/\\frac\{d\}\{dx\}|อนุพันธ์|derivative|ดิฟ|\bchain rule\b/.test(q)) {
227
+ if (lang === 'zh') {
228
+ return [
229
+ { label: '📐 链式法则与乘除导数法则', query: '总结链式法则 (Chain Rule) 与乘积、商的求导公式' },
230
+ { label: '📈 导数应用:极值与拐点', query: '如何利用一阶和二阶导数求解函数的极值点与拐点?' },
231
+ { label: '✏️ 隐函数求导与高阶导数', query: '详细解析隐函数求导与高阶导数的计算步骤' }
232
+ ];
233
+ }
234
+ if (lang === 'en') {
235
+ return [
236
+ { label: '📐 Chain Rule and Product/Quotient Rules', query: 'Explain the Chain Rule, Product Rule, and Quotient Rule in differentiation' },
237
+ { label: '📈 Optimization and Critical Points', query: 'How to use derivatives to find critical points and local extrema?' },
238
+ { label: '✏️ Implicit Differentiation', query: 'Explain how to perform implicit differentiation with examples' }
239
+ ];
240
+ }
241
+ return [
242
+ { label: '📐 กฎลูกโซ่และสูตรอนุพันธ์ผลคูณ/ผลหาร', query: 'สรุปการใช้กฎลูกโซ่ (Chain Rule) และสูตรอนุพันธ์ผลคูณผลหาร' },
243
+ { label: '📈 การประยุกต์อนุพันธ์หาจุดสูงสุด-ต่ำสุด', query: 'การประยุกต์ใช้อนุพันธ์หาจุดวิกฤตและค่าสูงสุดต่ำสุดของฟังก์ชัน' },
244
+ { label: '✏️ อนุพันธ์อันดับสูงและอนุพันธ์โดยปริยาย', query: 'อธิบายการหาอนุพันธ์อันดับสูงและอนุพันธ์โดยปริยาย' }
245
+ ];
246
+ }
247
+
248
+ if (/\\int|อินทิเกรต|integral|ปริพันธ์|\bby parts\b/.test(q)) {
249
+ if (lang === 'zh') {
250
+ return [
251
+ { label: '📐 分部积分法 (Integration by Parts)', query: '详细讲解分部积分法的核心公式与典型例题' },
252
+ { label: '🔄 换元积分法 (u-Substitution)', query: '如何灵活应用第一类与第二类换元积分法?' },
253
+ { label: '📊 微积分基本定理与定积分应用', query: '微积分基本定理的意义以及计算曲线下方图形面积的方法' }
254
+ ];
255
+ }
256
+ if (lang === 'en') {
257
+ return [
258
+ { label: '📐 Integration by Parts', query: 'Explain the Integration by Parts formula with examples' },
259
+ { label: '🔄 u-Substitution technique', query: 'How to choose u and compute integrals using u-substitution?' },
260
+ { label: '📊 Fundamental Theorem of Calculus', query: 'Explain the Fundamental Theorem of Calculus and area calculations' }
261
+ ];
262
+ }
263
+ return [
264
+ { label: '📐 เทคนิค Integration by Parts (แยกส่วน)', query: 'อธิบายเทคนิคการอินทิเกรตแบบแยกส่วน (Integration by Parts) พร้อมตัวอย่าง' },
265
+ { label: '🔄 การอินทิเกรตโดยแทนค่าตัวแปร u', query: 'วิธีอินทิเกรตด้วยการแทนค่าตัวแปร u (u-Substitution)' },
266
+ { label: '📊 ทฤษฎีบทหลักมูลของแคลคูลัสและการหาพื้นที่', query: 'ทฤษฎีบทหลักมูลของแคลคูลัสกับการคำนวณพื้นที่ใต้กราฟ' }
267
+ ];
268
+ }
269
+
270
+ if (/สมการ|equation|เมทริกซ์|matrix|พีชคณิต|algebra|พหุนาม|polynomial|quadratic|กำลังสอง/.test(q)) {
271
+ if (lang === 'zh') {
272
+ return [
273
+ { label: '📐 方程系统求解与验根方法', query: '多项式方程求解的标准步骤与根的检验方法' },
274
+ { label: '🔍 因式分解与配方法技巧', query: '二次与三次多项式因式分解的常用方法' },
275
+ { label: '📈 函数图象与几何根的判定', query: '如何结合函数图像直观理解方程根的分布?' }
276
+ ];
277
+ }
278
+ if (lang === 'en') {
279
+ return [
280
+ { label: '📐 Solving Equations & Verifying Roots', query: 'Step-by-step methods for solving equations and verifying solutions' },
281
+ { label: '🔍 Polynomial Factoring Techniques', query: 'Common techniques for factoring polynomials and completing the square' },
282
+ { label: '📈 Graphical Analysis of Solutions', query: 'How to graph equations to find roots and intersection points?' }
283
+ ];
284
+ }
285
+ return [
286
+ { label: '📐 ขั้นตอนการแก้สมการและวิธีตรวจคำตอบ', query: 'ขั้นตอนการแก้สมการและวิธีตรวจคำตอบอย่างเป็นระบบ' },
287
+ { label: '🔍 การแยกตัวประกอบพหุนามและสูตรกำลังสอง', query: 'สรุปสูตรการแยกตัวประกอบพหุนามกำลังสองและกำลังสาม' },
288
+ { label: '📈 การเขียนกราฟเพื่อวิเคราะห์คำตอบของฟังก์ชัน', query: 'วิธีวาดกราฟและหาจุดตัดเพื่อวิเคราะห์คำตอบของสมการ' }
289
+ ];
290
+ }
291
+
292
+ if (/kotlin/.test(q)) {
293
+ if (lang === 'zh') {
294
+ return [
295
+ { label: '💻 Kotlin 空安全 (Null Safety) 与数据类', query: '解析 Kotlin 中的空安全机制、智能转换与 Data Class' },
296
+ { label: '⚡ Kotlin 协程 (Coroutines) 异步编程', query: 'Kotlin 协程的基础概念与异步并发最佳实践' },
297
+ { label: '🛠️ Kotlin 作用域函数 (let/run/apply/also)', query: '对比 Kotlin 常用作用域函数 let, run, apply, also, with 的区别' }
298
+ ];
299
+ }
300
+ if (lang === 'en') {
301
+ return [
302
+ { label: '💻 Kotlin Null Safety & Data Classes', query: 'Explain Kotlin Null Safety, Smart Casts, and Data Classes with examples' },
303
+ { label: '⚡ Getting started with Kotlin Coroutines', query: 'Best practices for asynchronous programming with Kotlin Coroutines' },
304
+ { label: '🛠️ Scope Functions (let, run, apply, also, with)', query: 'Compare Kotlin scope functions: let, run, apply, also, with' }
305
+ ];
306
+ }
307
+ return [
308
+ { label: '💻 Null Safety และ Data Classes ใน Kotlin', query: 'อธิบายการจัดการ Null Safety, Smart Casts และ Data Classes ใน Kotlin' },
309
+ { label: '⚡ การใช้งาน Kotlin Coroutines เบื้องต้น', query: 'แนวทางการใช้งาน Kotlin Coroutines สำหรับงาน Asynchronous' },
310
+ { label: '🛠️ Scope Functions (let, run, apply, also, with)', query: 'เปรียบเทียบการใช้งาน Scope Functions (let, run, apply, also, with) ใน Kotlin' }
311
+ ];
312
+ }
313
+
314
+ if (/python|ไพธอน/.test(q)) {
315
+ if (lang === 'zh') {
316
+ return [
317
+ { label: '🐍 列表推导式与生成器 (Generators)', query: 'Python 列表推导式 (List Comprehension) 与生成器的高效用法' },
318
+ { label: '📦 Python 面向对象与装饰器 (Decorators)', query: '深入理解 Python 类继承与装饰器实现原理' },
319
+ { label: '🛡️ 异常处理与上下文管理器 (with)', query: 'Python 异常处理最佳实践与 Context Manager 编写' }
320
+ ];
321
+ }
322
+ if (lang === 'en') {
323
+ return [
324
+ { label: '🐍 List Comprehensions & Generators in Python', query: 'How to write efficient List Comprehensions and Generators in Python' },
325
+ { label: '📦 OOP & Decorators in Python', query: 'Explain Object-Oriented Programming and Decorators in Python' },
326
+ { label: '🛡️ Exception Handling & Context Managers', query: 'Best practices for Exception handling and custom Context Managers in Python' }
327
+ ];
328
+ }
329
+ return [
330
+ { label: '🐍 List Comprehension และ Generators ใน Python', query: 'แนะนำการใช้ List Comprehension และ Generators ใน Python ให้มีประสิทธิภาพ' },
331
+ { label: '📦 การเขียน OOP, Classes และ Decorators ใน Python', query: 'อธิบายการเขียน Class, Inheritance และ Decorators ใน Python' },
332
+ { label: '🛡️ การจัดการ Exception และ Context Managers (with)', query: 'วิธีจัดการ Exception และการสร้าง Context Manager ใน Python' }
333
+ ];
334
+ }
335
+
336
+ if (/javascript|typescript|\bjs\b|\bts\b|react|node\.?js/.test(q)) {
337
+ if (lang === 'zh') {
338
+ return [
339
+ { label: '⚡ Event Loop 与 Async/Await 原理', query: '深入讲解 JavaScript 事件循环 (Event Loop)、宏任务与微任务' },
340
+ { label: '🛡️ TypeScript 泛型 (Generics) 与类型推导', query: 'TypeScript 高阶泛型与常用 Utility Types 实践指南' },
341
+ { label: '✨ 现代 JavaScript (ES6+) 编码规范', query: '现代 JavaScript 核心语法特性与代码质量最佳实践' }
342
+ ];
343
+ }
344
+ if (lang === 'en') {
345
+ return [
346
+ { label: '⚡ Event Loop & Async/Await Mechanism', query: 'Explain JavaScript Event Loop, microtasks, and Async/Await under the hood' },
347
+ { label: '🛡️ TypeScript Generics & Utility Types', query: 'Advanced TypeScript Generics and practical Utility Types guide' },
348
+ { label: '✨ Modern JavaScript (ES6+) Best Practices', query: 'Key best practices for writing clean and modern JavaScript' }
349
+ ];
350
+ }
351
+ return [
352
+ { label: '⚡ การทำงานของ Event Loop, Promises และ Async/Await', query: 'อธิบายการทำงานของ Event Loop, Microtasks และ Async/Await ใน JavaScript' },
353
+ { label: '🛡️ TypeScript Generics และ Utility Types', query: 'แนะนำการใช้งาน Generics และ Utility Types ใน TypeScript' },
354
+ { label: '✨ Modern JavaScript (ES6+) Best Practices', query: 'แนะนำ Best Practices ในการเขียน Clean Modern JavaScript' }
355
+ ];
356
+ }
357
+
358
+ if (/เขียนโค้ด|เขียนโปรแกรม|coding|programming|อัลกอริทึม|algorithm|โครงสร้างข้อมูล|\bdata structure\b|\bbig-?o\b/.test(q)) {
359
+ if (lang === 'zh') {
360
+ return [
361
+ { label: '⏱️ 时间与空间复杂度 (Big-O) 分析', query: '如何系统分析算法的时间复杂度与空间复杂度 (Big-O)?' },
362
+ { label: '🌳 常用数据结构选型对比', query: '数组、哈希表、链表和二叉树在不同场景下的选型权衡' },
363
+ { label: '🧪 单元测试编写与代码调试技巧', query: '如何编写规范的单元测试以及提高 Debug 效率?' }
364
+ ];
365
+ }
366
+ if (lang === 'en') {
367
+ return [
368
+ { label: '⏱️ Big-O Time & Space Complexity Analysis', query: 'How to analyze Big-O time and space complexity of algorithms' },
369
+ { label: '🌳 Data Structures Trade-offs', query: 'Comparing trade-offs between Arrays, HashMaps, Sets, and Trees' },
370
+ { label: '🧪 Unit Testing & Debugging Best Practices', query: 'Best practices for writing unit tests and debugging code efficiently' }
371
+ ];
372
+ }
373
+ return [
374
+ { label: '⏱️ การวิเคราะห์ Time & Space Complexity (Big-O)', query: 'วิธีวิเคราะห์ Time และ Space Complexity ด้วย Big-O Notation' },
375
+ { label: '🌳 การเลือกใช้โครงสร้างข้อมูล (Data Structures)', query: 'เปรียบเทียบการเลือกใช้ Array, Map, Set และ Tree ในการเขียนโปรแกรม' },
376
+ { label: '🧪 แนวทางการเขียน Unit Test และ Debugging', query: 'แนวทางการเขียน Unit Test และเทคนิคการ Debug โค้ดอย่างมีประสิทธิภาพ' }
377
+ ];
378
+ }
379
+
380
+ if (/ฟิสิกส์|physics|เคมี|chemistry|ชีววิทยา|biology|กลศาสตร์|เทอร์โมไดนามิกส์|แรงโน้มถ่วง|ตารางธาตุ/.test(q)) {
381
+ if (lang === 'zh') {
382
+ return [
383
+ { label: '🔬 核心科学原理与理论背景', query: '深入解释这一现象背后的物理与科学理论原理' },
384
+ { label: '📐 计算公式推导与标准单位换算', query: '给出相关的物理计算公式、推导过程与国际标准单位 (SI)' },
385
+ { label: '🚀 前沿科技与现实生活中的应用', query: '该科学定律在现代工程、科技与日常生活中有哪些实际应用?' }
386
+ ];
387
+ }
388
+ if (lang === 'en') {
389
+ return [
390
+ { label: '🔬 Core Scientific Principles & Theory', query: 'Explain the fundamental scientific and physical theory behind this topic' },
391
+ { label: '📐 Formulas & SI Unit Conversion', query: 'Show formula derivation and SI unit conversion with practical examples' },
392
+ { label: '🚀 Real-world & Technological Applications', query: 'How is this scientific principle applied in modern technology and daily life?' }
393
+ ];
394
+ }
395
+ return [
396
+ { label: '🔬 หลักการและทฤษฎีทางวิทยาศาสตร์ที่เกี่ยวข้อง', query: 'ช่วยอธิบายหลักการและทฤษฎีทางวิทยาศาสตร์ที่อยู่เบื้องหลังเรื่องนี้อย่างละเอียด' },
397
+ { label: '📐 ขั้นตอนการคำนวณและการแปลงหน่วยมาตรฐาน (SI)', query: 'แสดงขั้นตอนการคำนวณพร้อมสูตรและการแปลงหน่วยมาตรฐาน SI' },
398
+ { label: '🚀 การประยุกต์ใช้ในเทคโนโลยีจริงและชีวิตประจำวัน', query: 'หลักการทางวิทยาศาสตร์นี้ถูกนำไปประยุกต์ใช้ในเทคโนโลยีปัจจุบันอย่างไร' }
399
+ ];
400
+ }
401
+
402
+ return null;
403
+ }
404
+
405
+ function cleanOffTopicQuery(raw) {
406
+ let text = String(raw || '').replace(/\s+/g, ' ').trim();
407
+ text = text
408
+ .replace(/^(?:explain|give more examples of|what else should i know about|summarize the key points of|compare|reliable sources about|tell me more about|help me understand|find the value of|calculate|evaluate|solve|compute|please solve|how to solve|teach me|how do i|what is the value of)\s+/i, '')
409
+ .replace(/\s+(?:in more detail|in detail|with related topics|please|step by step|for beginners)$/i, '')
410
+ .replace(/^(?:จงหาค่าของ|จงหาค่า|จงคำนวณ|คำนวณ|หาค่าของ|หาค่า|ช่วยหา|แก้โจทย์|ช่วยแก้โจทย์|จงแก้|แก้สมการ|ช่วยสอนฉันเขียนโค้ด|ช่วยสอนฉัน|ช่วยสอน|สอนฉัน|สอนวิธี|วิธีทำ|วิธี|ขอวิธี|อยากทราบ|อยากรู้|ช่วยบอก|บอกวิธี|ช่วยทำ|ทำโจทย์|อธิบายเรื่อง|ยกตัวอย่างเพิ่มเติมเกี่ยวกับ|มีอะไรที่ควรรู้เพิ่มเกี่ยวกับ|สรุปประเด็นสำคัญของ|เปรียบเทียบ|แหล่งข้อมูลน่าเชื่อถือเกี่ยวกับ|ช่วยอธิบาย|เล่าเรื่อง|แนะนำ)\s*/i, '')
411
+ .replace(/\s*(?:ให้ละเอียดกว่านี้|อีกไหม|กับเรื่องใกล้เคียง|เพิ่มเติม|หน่อย|ง่ายๆนะ|ง่ายๆ|หน่อยครับ|หน่อยค่ะ|ครับ|ค่ะ|อย่างไร|ได้ไหม|ทำยังไง|มีอะไรบ้าง|คืออะไร)*$/i, '')
412
+ .replace(/^(?:更详细地解释|举更多关于|关于|总结|比较|求|计算|求解|请问|怎么解|如何计算)\s*/, '')
413
+ .replace(/\s*(?:的例子|还有什么需要注意的|的要点|与相关主题|的可靠来源有哪些|一下)*$/, '')
414
+ .trim();
415
+
416
+ if (text.length > 45) {
417
+ const firstDollar = text.indexOf('$');
418
+ if (firstDollar !== -1 && firstDollar < 45) {
419
+ const closingDollar = text.indexOf('$', firstDollar + 1);
420
+ if (closingDollar !== -1 && closingDollar + 1 <= 65) {
421
+ text = text.slice(0, closingDollar + 1).trim();
422
+ } else if (firstDollar > 3) {
423
+ text = text.slice(0, firstDollar).trim();
424
+ }
425
+ }
426
+ if (text.length > 45) {
427
+ const sub = text.slice(0, 45);
428
+ const lastSpace = sub.lastIndexOf(' ');
429
+ let cut = (lastSpace > 18) ? sub.slice(0, lastSpace) : sub;
430
+ cut = cut.replace(/\$[^$]*$/, '').replace(/[\\{+\-*=_]+$/, '').trim();
431
+ text = cut || sub.trim();
432
+ }
433
+ }
434
+ return text;
435
+ }
436
+
437
+ export function buildOffTopicFollowUps(query, explicitLang) {
438
+ const lang = detectFollowUpLang(query, explicitLang);
439
+ const specialized = detectSpecializedOpenDomainFollowUps(query, lang);
440
+ if (specialized && specialized.length > 0) {
441
+ return specialized;
442
+ }
443
+ const topic = cleanOffTopicQuery(query);
444
+ if (!topic) return [];
445
+ const templates = lang === 'zh'
446
+ ? [`更详细地解释${topic}`, `举更多关于${topic}的例子`, `关于${topic}还有什么需要注意的`, `总结${topic}的要点`, `比较${topic}与相关主题`, `关于${topic}的可靠来源有哪些`]
447
+ : lang === 'th'
448
+ ? [`อธิบายเรื่อง${topic}ให้ละเอียดกว่านี้`, `ยกตัวอย่างเพิ่มเติมเกี่ยวกับ${topic}`, `มีอะไรที่ควรรู้เพิ่มเกี่ยวกับ${topic}อีกไหม`, `สรุปประเด็นสำคัญของ${topic}`, `เปรียบเทียบ${topic}กับเรื่องใกล้เคียง`, `แหล่งข้อมูลน่าเชื่อถือเกี่ยวกับ${topic}`]
449
+ : [`Explain ${topic} in more detail`, `Give more examples of ${topic}`, `What else should I know about ${topic}`, `Summarize the key points of ${topic}`, `Compare ${topic} with related topics`, `Reliable sources about ${topic}`];
450
+ return templates.map(q => ({ label: '💡 ' + q, query: q }));
451
+ }
452
+
453
+ const TOPIC_CANDIDATES = [
454
+ {
455
+ key: 'general_dramas',
456
+ keywords: ['ซีรีส์', 'ละคร', 'dramas', 'drama', 'series', '作品', '代表作', '影视', '剧集', '主演', 'acting'],
457
+ tools: ['search_works', 'get_work_detail'],
458
+ toolMatcher: (t) => t.name === 'search_works' || t.name === 'get_work_detail',
459
+ suggestions: {
460
+ th: [
461
+ { label: '🎬 แนะนำซีรีส์ยอดนิยมที่ไม่ควรพลาด', query: 'แนะนำซีรีส์ยอดนิยมและบทบาทมาสเตอร์พีซของจวีจิ้งอี' },
462
+ { label: '🌸 บทบาทในฮวาเจียนหลิง (In Blossom)', query: 'บทบาทของหยางไฉ่เวยและซ่างกวนจื่อในฮวาเจียนหลิงมีความน่าสนใจอย่างไร' },
463
+ { label: '📺 ซีรีส์ใหม่ล่าสุด 2026', query: 'อัปเดตซีรีส์ใหม่ล่าสุดของจวีจิ้งอีอย่าง Veil of Shadows และ The Eternal Fragrance' }
464
+ ],
465
+ zh: [
466
+ { label: '🎬 经典影视代表作盘点', query: '鞠婧祎主演过哪些高口碑与高人气的经典影视剧?' },
467
+ { label: '🌸 《花间令》双面角色解析', query: '鞠婧祎在《花间令》中如何塑造杨采薇与上官芷?' },
468
+ { label: '📺 2026 待播新剧动态', query: '了解鞠婧祎 2026 待播大剧《月鳞绮纪》与《千香》最新动态' }
469
+ ],
470
+ en: [
471
+ { label: '🎬 Top Recommended Drama Series', query: 'What are Ju Jingyi\'s most popular and recommended drama series?' },
472
+ { label: '🌸 In Blossom Dual Role', query: 'How was Ju Jingyi\'s dual-identity performance in In Blossom?' },
473
+ { label: '📺 Upcoming 2026 Dramas', query: 'Latest updates on upcoming dramas Veil of Shadows and The Eternal Fragrance' }
474
+ ]
475
+ }
476
+ },
477
+ {
478
+ key: 'in_blossom',
479
+ keywords: ['ฮวาเจียนลิ่ง', 'ฮวาเจียนหลิง', 'in blossom', '花间令', 'หยางไฉ่เวย', 'ซ่างกวนจื่อ', 'พานเยว่'],
480
+ tools: ['search_works', 'get_work_detail'],
481
+ toolMatcher: (t) => {
482
+ const txt = JSON.stringify(t.result || '').toLowerCase();
483
+ return txt.includes('in blossom') || txt.includes('花间令') || txt.includes('ฮวาเจียน');
484
+ },
485
+ suggestions: {
486
+ th: [
487
+ { label: '🌸 ตัวตนกับรูปลักษณ์', query: 'วิเคราะห์การถ่ายทอดตัวตนของหยางไฉ่เวยภายใต้รูปลักษณ์ซ่างกวนจื่อในฮวาเจียนลิ่ง' },
488
+ { label: '🎵 เพลงประกอบฮวาเจียนลิ่ง', query: 'เพลงประกอบซีรีส์ ฮวาเจียนลิ่ง (In Blossom OST) ทั้งหมดมีเพลงอะไรบ้าง' },
489
+ { label: '🎬 ปมเปลี่ยนใบหน้า', query: 'อธิบายว่าปมเปลี่ยนใบหน้าในฮวาเจียนลิ่งส่งผลต่อการสืบคดีและตัวตนของหยางไฉ่เวยอย่างไร' }
490
+ ],
491
+ zh: [
492
+ { label: '🌸 角色双重身份解析', query: '解析鞠婧祎在《花间令》中如何演绎上官芷外貌下的杨采薇内核' },
493
+ { label: '🎵 《花间令》原声音乐', query: '电视剧《花间令》的所有原声带与插曲有哪些?' },
494
+ { label: '🎬 换脸悬疑主线', query: '换脸案件如何推动《花间令》破案主线与人物命运走向?' }
495
+ ],
496
+ en: [
497
+ { label: '🌸 Dual Identity Nuances', query: 'Analyze Ju Jingyi\'s portrayal of Yang Caiwei behind Shangguan Zhi\'s appearance in In Blossom' },
498
+ { label: '🎵 In Blossom OSTs', query: 'What are the official soundtrack songs and OSTs from In Blossom?' },
499
+ { label: '🎬 Face-Swap Plot Mystery', query: 'How does the face-swap mystery affect the investigation and character arc in In Blossom?' }
500
+ ]
501
+ }
502
+ },
503
+ {
504
+ key: 'yunxi',
505
+ keywords: ['หยุนซี', 'yunxi', '芸汐传', 'หานหยุนซี', 'จางเจ๋อฮั่น', 'หลงเฟยเย่', '叹云兮'],
506
+ tools: ['search_works', 'get_work_detail'],
507
+ toolMatcher: (t) => {
508
+ const txt = JSON.stringify(t.result || '').toLowerCase();
509
+ return txt.includes('yunxi') || txt.includes('芸汐');
510
+ },
511
+ suggestions: {
512
+ th: [
513
+ { label: '🌿 ทักษะพิษและวิชาแพทย์', query: 'เล่าจุดเด่นวิชาแพทย์และทักษะการใช้พิษของหานหยุนซีในตำนานหยุนซี' },
514
+ { label: '🎵 เพลง 叹云兮 (Sigh of Yunxi)', query: 'เบื้องหลังและเสน่ห์ของเพลง 叹云兮 (Sigh of Yunxi) ขับร้องโดยจวีจิ้งอี' },
515
+ { label: '🌟 เคมีคู่กับหลงเฟยเย่', query: 'เคมีการแสดงระหว่าง จวีจิ้งอี และ จางเจ๋อฮั่น ในซีรีส์ตำนานหยุนซี' }
516
+ ],
517
+ zh: [
518
+ { label: '🌿 韩芸汐毒术与医道', query: '解析《芸汐传》中韩芸汐的高超解毒医术与角色个性' },
519
+ { label: '🎵 经典名曲《叹云兮》', query: '鞠婧祎演唱的《芸汐传》片尾曲《叹云兮》背后的故事与反响' },
520
+ { label: '🌟 西芹 CP 经典化学反应', query: '鞠婧祎与张哲瀚在《芸汐传》中的合作火花与名场面' }
521
+ ],
522
+ en: [
523
+ { label: '🌿 Medical & Poison Lore', query: 'Highlight Han Yunxi\'s unique medical and poison mastery in Legend of Yunxi' },
524
+ { label: '🎵 Sigh of Yunxi Theme', query: 'Behind the iconic OST "Sigh of Yunxi" (叹云兮) sung by Ju Jingyi' },
525
+ { label: '🌟 Lead Romance Dynamics', query: 'The acting chemistry and memorable moments between Ju Jingyi and Zhang Zhehan in Legend of Yunxi' }
526
+ ]
527
+ }
528
+ },
529
+ {
530
+ key: 'white_snake',
531
+ keywords: ['นางพญางูขาว', 'white snake', '新白娘子传奇', 'ไป๋ซู่เจิน', 'สวี่เซียน', '千年等一回'],
532
+ suggestions: {
533
+ th: [
534
+ { label: '🐍 เสน่ห์บทบาทไป๋ซู่เจิน', query: 'ความโดดเด่นและชุดแต่งกายสีขาวอันเป็นเอกลักษณ์ของไป๋ซู่เจิน 2019' },
535
+ { label: '🎵 เพลง 千年等一回', query: 'เพลง 千年等一回 เวอร์ชันจวีจิ้งอี มีความไพเราะและกระแสตอบรับอย่างไร' },
536
+ { label: '✨ ฉากตำนานสะพานขาด', query: 'เล่าฉากพบกันบนสะพานขาดระหว่างไป๋ซู่เจินและสวี่เซียน' }
537
+ ],
538
+ zh: [
539
+ { label: '🐍 白素贞仙气造型', query: '2019版《新白娘子传奇》鞠婧祎白娘子造型与古典仙气' },
540
+ { label: '🎵 重绎《千年等一回》', query: '鞠婧祎重新演绎经典仙侠曲《千年等一回》的编曲与演唱特点' },
541
+ { label: '✨ 断桥相会经典名场面', query: '解析新版白娘子与许仙断桥初遇的名场面处理' }
542
+ ],
543
+ en: [
544
+ { label: '🐍 Bai Suzhen Elegance', query: 'The timeless white aesthetic and character elegance of Bai Suzhen 2019' },
545
+ { label: '🎵 A Millennium\'s Wait OST', query: 'Ju Jingyi\'s vocal delivery in the remastered theme "A Millennium\'s Wait" (千年等一回)' },
546
+ { label: '✨ Broken Bridge Scene', query: 'The iconic Broken Bridge encounter scene between Bai Suzhen and Xu Xian' }
547
+ ]
548
+ }
549
+ },
550
+ {
551
+ key: 'veil_of_shadows',
552
+ keywords: ['veil of shadows', '月鳞绮纪', 'ลู่หวู่อี้', 'เถียนเจียรุ่ย', 'ไม่ทุกข์ตรม', '不苦', 'ม่านเงาจันทรา'],
553
+ suggestions: {
554
+ th: [
555
+ { label: '🗡️ บทบาทลู่หวู่อี้ 2026', query: 'บทบาท ลู่หวู่อี้ ในซีรีส์ Veil of Shadows (月鳞绮纪) มีความท้าทายอย่างไร' },
556
+ { label: '🎵 เพลงคู่ ไม่ทุกข์ตรม (不苦)', query: 'เบื้องหลังเพลง 不苦 (Not Suffering) ร้องคู่กับ เถียนเจียรุ่ย' },
557
+ { label: '📺 กำหนดการฉายและแพลตฟอร์ม', query: 'อัปเดตสถานะและแพลตฟอร์มการออกอากาศของ Veil of Shadows' }
558
+ ],
559
+ zh: [
560
+ { label: '🗡️ 陆吾一角色看点', query: '鞠婧祎在待播大剧《月鳞绮纪》中饰演的陆吾一有哪些性格与打戏挑战?' },
561
+ { label: '🎵 合唱插曲《不苦》', query: '鞠婧祎与田嘉瑞合唱的《月鳞绮纪》主题插曲《不苦》幕后制作' },
562
+ { label: '📺 定档与播出动态', query: '更新《月鳞绮纪》的最新制作进度、播出平台与定档动态' }
563
+ ],
564
+ en: [
565
+ { label: '🗡️ Lu Wuyi Role Depth', query: 'What are the key acting challenges of Lu Wuyi in Veil of Shadows (月鳞绮纪)?' },
566
+ { label: '🎵 Duet Track "Not Suffering"', query: 'Behind the duet soundtrack "Not Suffering" (不苦) with Tian Jiarui' },
567
+ { label: '📺 Release & Platform Updates', query: 'Latest broadcasting platform and release schedule updates for Veil of Shadows' }
568
+ ]
569
+ }
570
+ },
571
+ {
572
+ key: 'eternal_fragrance',
573
+ keywords: ['eternal fragrance', '千香', 'พันกลิ่นบุปผา', '千香引', 'ซ่งเวยหลง'],
574
+ suggestions: {
575
+ th: [
576
+ { label: '🌸 บทบาทในพันกลิ่นบุปผา', query: 'บทบาทของจวีจิ้งอีในซีรีส์ The Eternal Fragrance (พันกลิ่นบุปผา) เป็นอย่างไร' },
577
+ { label: '🎵 เพลง 千香 (OST 2026)', query: 'เปิดเพลง 千香 (The Eternal Fragrance) OST ล่าสุดปี 2026' },
578
+ { label: '📺 ซีรีส์ฟอร์มยักษ์ Youku S+', query: 'รายละเอียดความยิ่งใหญ่ของซีรีส์ The Eternal Fragrance ทาง Youku' }
579
+ ],
580
+ zh: [
581
+ { label: '🌸 《千香》大女主设定', query: '鞠婧祎在优酷S+级仙侠巨制《千香》中的角色设定与世界观' },
582
+ { label: '🎵 电视剧原声音乐', query: '《千香》电视剧主题曲与鞠婧祎个人献唱原声' },
583
+ { label: '✨ 与宋威龙合作火花', query: '鞠婧祎与宋威龙在《千香》中的搭档看点与期待度' }
584
+ ],
585
+ en: [
586
+ { label: '🌸 The Eternal Fragrance Role', query: 'Details on Ju Jingyi\'s lead role and character setting in The Eternal Fragrance' },
587
+ { label: '🎵 The Eternal Fragrance OST', query: 'Original soundtrack and theme song details for The Eternal Fragrance' },
588
+ { label: '📺 Youku S+ Fantasy Epic', query: 'Production scale and streaming details for Youku\'s The Eternal Fragrance' }
589
+ ]
590
+ }
591
+ },
592
+ {
593
+ key: 'sword_fairy_4',
594
+ keywords: ['sword and fairy', '仙剑四', 'เซียนกระบี่', 'หานหลิงซา', 'เฉินเจ๋อหย่วน'],
595
+ suggestions: {
596
+ th: [
597
+ { label: '🗡️ บทบาทหานหลิงซา', query: 'ความสดใสและความเสียสละของ หานหลิงซา ในเซียนกระบี่พิชิตมาร 4 (Sword and Fairy 4)' },
598
+ { label: '🎵 เพลง 如纱 (Ru Sha)', query: 'เพลงธีม 如纱 (Ru Sha) ประจำตัวหานหลิงซา ถ่ายทอดอารมณ์ความรู้สึกอย่างไร' },
599
+ { label: '✨ เคมีคู่กับเฉินเจ๋อหย่วน', query: 'เคมีการแสดงระหว่าง จวีจิ้งอี และ เฉินเจ๋อหย่วน ในเซียนกระบี่ 4' }
600
+ ],
601
+ zh: [
602
+ { label: '🗡️ 韩菱纱灵动人设', query: '鞠婧祎在《仙剑四》中如何还原韩菱纱的率真、重情与命运宿命' },
603
+ { label: '🎵 人物主题曲《如纱》', query: '韩菱纱人物主题曲《如纱》的演唱情绪与意境解析' },
604
+ { label: '✨ 仙剑经典情怀重现', query: '《仙剑四》电视剧对仙剑IP经典剧情与场景的还原呈现' }
605
+ ],
606
+ en: [
607
+ { label: '🗡️ Han Lingsha Character', query: 'Ju Jingyi\'s portrayal of the spirited yet tragic Han Lingsha in Sword and Fairy 4' },
608
+ { label: '🎵 Character Theme "Ru Sha"', query: 'Behind the character theme song "Ru Sha" (如纱) sung by Ju Jingyi' },
609
+ { label: '✨ Xianxia Fantasy Highlights', query: 'Key fantasy arcs and chemistry in Sword and Fairy 4' }
610
+ ]
611
+ }
612
+ },
613
+ {
614
+ key: 'beauty_of_resilience',
615
+ keywords: ['beauty of resilience', '花戎', 'ฮวาหรง', 'เว่ยจือ', 'ฟีนิกซ์', 'กัวจวิ้นเฉิน', 'ตำนานเลือดฟีนิกซ์'],
616
+ suggestions: {
617
+ th: [
618
+ { label: '🪶 3 ชาติภพของเว่ยจือ', query: 'สรุปการแปลงร่างและพัฒนาการ 3 ชาติภพของเว่ยจือในตำนานเลือดฟีนิกซ์ (花戎)' },
619
+ { label: '🎵 เพลง 误长生 & 仍寻', query: 'เพลง 误长生 (Longevity By Mistake) และ 仍寻 (Still Searching) ในซีรีส์ตำนานเลือดฟีนิกซ์มีความหมายอย่างไร' },
620
+ { label: '✨ ฉากแปลงร่างฟีนิกซ์', query: 'ฉากแปลงร่างเป็นฟีนิกซ์อันงดงามของเว่ยจือในฮวาหรง' }
621
+ ],
622
+ zh: [
623
+ { label: '🪶 魏枝三世涅槃历程', query: '解析《花戎》中魏枝从凡间小桑染到凤凰真身的蜕变成长' },
624
+ { label: '🎵 原声插曲《误长生》', query: '鞠婧祎演唱的《花戎》主题曲《误长生》与人物心境' },
625
+ { label: '✨ 凤凰涅槃名场面', query: '《花戎》中震撼的凤凰展翅涅槃特效与高光名场面' }
626
+ ],
627
+ en: [
628
+ { label: '🪶 Three Lives of Wei Zhi', query: 'The character evolution of Wei Zhi across three reincarnations in Beauty of Resilience' },
629
+ { label: '🎵 Theme "Longevity By Mistake"', query: 'Behind the emotional theme song "Longevity By Mistake" (误长生)' },
630
+ { label: '✨ Phoenix Rebirth Visuals', query: 'The signature phoenix transformation scenes in Beauty of Resilience' }
631
+ ]
632
+ }
633
+ },
634
+ {
635
+ key: 'ruyi_pavilion',
636
+ keywords: ['ruyi pavilion', '如意芳霏', 'กรุ่นรักกลิ่นบุปผา', 'ฟู่หรง'],
637
+ suggestions: {
638
+ th: [
639
+ { label: '🌸 บทบาทฟู่หรง', query: 'เล่าบทบาทและความน่ารักของ ฟู่หรง ในซีรีส์ กรุ่นรักกลิ่นบุปผา (The Blooms at Ruyi Pavilion)' },
640
+ { label: '🎵 เพลง 芙蓉 & 古画', query: 'รวมเพลงประกอบซีรีส์ กรุ่นรักกลิ่นบุปผา ทั้ง 芙蓉, 古画 และ 梦渡' },
641
+ { label: '👗 แฟชั่นชุดโบราณในเรื่อง', query: 'ดีไซน์ชุดโบราณและเครื่องประดับของฟู่หรงในกรุ่นรักกลิ่นบุปผา' }
642
+ ],
643
+ zh: [
644
+ { label: '🌸 傅容灵动角色魅力', query: '解析《如意芳霏》中傅容聪慧坚韧的人设特点' },
645
+ { label: '🎵 电视剧原声《古画》', query: '鞠婧祎演唱的《古画》《芙蓉》等古风金曲解析' },
646
+ { label: '👗 国风服化道美学', query: '《如意芳霏》中的绝美汉服造型与古风美学' }
647
+ ],
648
+ en: [
649
+ { label: '🌸 Fu Rong Character Charm', query: 'The wit and charm of Fu Rong in The Blooms at Ruyi Pavilion' },
650
+ { label: '🎵 Iconic Traditional OSTs', query: 'The soundtrack songs "Ancient Painting" (古画) and "Lotus" (芙蓉)' },
651
+ { label: '👗 Traditional Costume Aesthetics', query: 'Costume design and aesthetic details in The Blooms at Ruyi Pavilion' }
652
+ ]
653
+ }
654
+ },
655
+ {
656
+ key: 'rebirth_for_you',
657
+ keywords: ['rebirth for you', '嘉南传', 'มู่หนานจือ', 'เจียงเป่าหนิง', 'กำเนิดใหม่เพื่อเธอ'],
658
+ suggestions: {
659
+ th: [
660
+ { label: '👑 บทบาทเจียงเป่าหนิง', query: 'บทบาท เจียงเป่าหนิง ในซีรีส์ กำเนิดใหม่เพื่อเธอ (Rebirth For You) มีเสน่ห์และพัฒนาการอย่างไร' },
661
+ { label: '🎵 เพลงประกอบซีรีส์', query: 'รวมเพลงประกอบซีรีส์ Rebirth For You (มู่หนานจือ) ที่ไพเราะ' },
662
+ { label: '✨ มงกุฎและชุดราชสำนัก', query: 'ความประณีตของชุดราชสำนักและเครื่องประดับศีรษะของเจียงเป่าหนิงในเรื่อง' }
663
+ ],
664
+ zh: [
665
+ { label: '👑 姜保宁郡主智谋', query: '解析《嘉南传》中姜保宁郡主独立机智、精通机关术的人设亮点' },
666
+ { label: '🎵 影视原声配乐', query: '《嘉南传》电视剧原声带精选' },
667
+ { label: '✨ 华美宫廷服饰造型', query: '盘点姜保宁在剧中的华丽凤冠与百变宫廷造型' }
668
+ ],
669
+ en: [
670
+ { label: '👑 Jiang Baoning Role', query: 'Jiang Baoning\'s intellect, courage, and mechanical genius in Rebirth For You' },
671
+ { label: '🎵 Rebirth For You Soundtracks', query: 'The popular original soundtracks from Rebirth For You' },
672
+ { label: '✨ Regal Costumes & Crowns', query: 'The exquisite imperial headwear and costume designs in Rebirth For You' }
673
+ ]
674
+ }
675
+ },
676
+ {
677
+ key: 'fashion_brand',
678
+ keywords: ['versace', 'fred', 'fred paris', 'prsr', 'pasha', '帕莎', 'แบรนด์', 'แฟชั่น', 'พรีเซนเตอร์', 'แอมบาสเดอร์', 'เครื่องประดับ', 'เมคอัพ', 'makeup', 'พรมแดง', 'ambassador', 'commercial', 'brand'],
679
+ tools: ['get_brand_endorsements'],
680
+ toolMatcher: (t) => t.name === 'get_brand_endorsements',
681
+ suggestions: {
682
+ th: [
683
+ { label: '💎 บทบาทแบรนด์แอมบาสเดอร์', query: 'เจาะลึกบทบาท จวีจิ้งอี กับตำแหน่ง Brand Ambassador ระดับโลกอย่าง Versace, FRED Paris และ PRSR' },
684
+ { label: '💄 สไตล์เมคอัพ Ju Jingyi', query: 'วิเคราะห์อิทธิพลของสไตล์การแต่งหน้า Ju Jingyi Makeup ที่ฮิตไปทั่วโลก' },
685
+ { label: '👗 แบรนด์พรีเซนเตอร์ทั้งหมด', query: 'สรุปแบรนด์ที่จวีจิ้งอีเป็นพรีเซนเตอร์และแอมบาสเดอร์ในปัจจุบัน' }
686
+ ],
687
+ zh: [
688
+ { label: '💎 顶级时尚品牌代言', query: '盘点鞠婧祎担任 Versace、FRED 斐登珠宝、PRSR 帕莎等全球品牌代言人动态' },
689
+ { label: '💄 鞠式美学与出圈妆容', query: '解析风靡海内外的「鞠式美学」与倒三角眼妆出圈密码' },
690
+ { label: '👗 品牌商务全汇总', query: '总结鞠婧祎目前的所有商业代言、大使身份与合作品牌' }
691
+ ],
692
+ en: [
693
+ { label: '💎 Global Ambassadorships', query: 'Explore Ju Jingyi\'s global brand ambassador roles with Versace, FRED Paris, and PRSR' },
694
+ { label: '💄 Viral Ju Jingyi Makeup', query: 'Analyze the global beauty influence of the signature Ju Jingyi Makeup trend' },
695
+ { label: '👗 Complete Brand Portfolio', query: 'Summary of all current commercial endorsements and brand ambassadorships' }
696
+ ]
697
+ }
698
+ },
699
+ {
700
+ key: 'music_stage',
701
+ keywords: ['เพลง', 'song', 'singles', 'find the way', 'everyday', '0.2s', 'ost', 'อัลบั้ม', 'jukebox', 'ดนตรี', 'เต้น', 'คอนเสิร์ต', 'stage', 'สเตจ', '红昭愿', '恋爱告急', 'music', 'live'],
702
+ tools: ['search_discography', 'get_song_detail'],
703
+ toolMatcher: (t) => t.name === 'search_discography' || t.name === 'get_song_detail',
704
+ suggestions: {
705
+ th: [
706
+ { label: '🚀 ซิงเกิล FIND THE WAY 2026', query: 'เบื้องหลังเพลง FIND THE WAY และคอนเซปต์ใหม่ปี 2026' },
707
+ { label: '🎧 ซิงเกิลเดี่ยวและ OST ทั้งหมด', query: 'รวมเพลงเดี่ยวและเพลงประกอบซีรีส์ยอดนิยมของจวีจิ้งอี' },
708
+ { label: '💃 สเตจเต้น 红昭愿 ในตำนาน', query: 'สเตจการเต้นเพลง 红昭愿 (Hong Zhao Yuan) ชุดแดงที่เป็นตำนาน' }
709
+ ],
710
+ zh: [
711
+ { label: '🚀 2026 全新单曲 FIND THE WAY', query: '解析鞠婧祎 2026 全新单曲《FIND THE WAY》的音乐概念与突破' },
712
+ { label: '🎧 Solo 单曲与 OST 全纪录', query: '盘点鞠婧祎个人全部单曲专辑与出圈电视剧原声带' },
713
+ { label: '💃 神级舞台《红昭愿》', query: '红衣封神的《红昭愿》国风唱跳舞台背后的出圈故事' }
714
+ ],
715
+ en: [
716
+ { label: '🚀 FIND THE WAY 2026 Single', query: 'Behind the music style and concept of Ju Jingyi\'s 2026 single FIND THE WAY' },
717
+ { label: '🎧 Complete Solo & OST Catalog', query: 'Explore Ju Jingyi\'s solo discography and hit drama soundtrack releases' },
718
+ { label: '💃 Iconic Stage: Hong Zhao Yuan', query: 'The viral red-outfit stage performance of "Hong Zhao Yuan" (红昭愿)' }
719
+ ]
720
+ }
721
+ },
722
+ {
723
+ key: 'milestones_awards',
724
+ keywords: ['รางวัล', 'award', 'เกียรติยศ', 'snh48', 'เลือกตั้ง', 'weibo', 'ความสำเร็จ', 'hall of fame', 'สตูดิโอ', 'studio'],
725
+ tools: ['get_awards', 'get_career_timeline'],
726
+ suggestions: {
727
+ th: [
728
+ { label: '🏆 ทำเนียบรางวัลสำคัญ', query: 'สรุปรางวัลด้านการแสดง ดนตรี และความนิยมทั้งหมดของจวีจิ้งอี' },
729
+ { label: '👑 ก้าวสู่ Hall of Fame', query: 'ประวัติศาสตร์การชนะเลือกตั้งอันดับ 1 สองสมัยซ้อนใน SNH48 สู่หอเกียรติยศ' },
730
+ { label: '⭐ การก่อตั้ง Stars Dream Studio', query: 'เส้นทางการเปิดสตูดิโออิสระและการเติบโตในฐานะศิลปินเดี่ยว' }
731
+ ],
732
+ zh: [
733
+ { label: '🏆 历年荣誉与奖项汇总', query: '盘点鞠婧祎在影视、音乐与商业领域斩获的重要奖项' },
734
+ { label: '👑 SNH48 登顶与晋升名人堂', query: '两届总选举第一连霸并成为名人堂首位成员的历史时刻' },
735
+ { label: '⭐ 个人工作室与独立发展', query: '成立个人工作室后的独立发展历程与多元突破' }
736
+ ],
737
+ en: [
738
+ { label: '🏆 Awards & Honors Hall', query: 'Comprehensive summary of major awards and recognitions won by Ju Jingyi' },
739
+ { label: '👑 SNH48 Consecutive No.1 & Hall of Fame', query: 'Historic consecutive election victories and Hall of Fame ascension in SNH48' },
740
+ { label: '⭐ Independent Studio Evolution', query: 'The founding of her independent studio and solo artist milestones' }
741
+ ]
742
+ }
743
+ }
744
+ ];
745
+
746
+ export function generateDynamicFollowUps({ query = '', answer = '', tools = [], recentKeys = null, lang = null } = {}) {
150
747
  const combined = (query + ' ' + answer).toLowerCase();
151
748
  const suggestions = [];
749
+ const offTopic = isOffTopicChat(combined, tools);
750
+ const detectedLang = detectFollowUpLang(combined, lang);
152
751
 
153
-
154
- if (combined.includes('ฮวาเจียนหลิง') || combined.includes('in blossom') || combined.includes('花间令') || combined.includes('หยางไฉ่เวย') || combined.includes('ซ่างกวนจื่อ')) {
155
- suggestions.push(
156
- { label: '🌸 ตัวตนกับรูปลักษณ์', query: 'วิเคราะห์การถ่ายทอดตัวตนของหยางไฉ่เวยภายใต้รูปลักษณ์ซ่างกวนจื่อในฮวาเจียนลิ่ง' },
157
- { label: '🎵 เพลงประกอบฮวาเจียนลิ่ง', query: 'เพลงประกอบซีรีส์ ฮวาเจียนลิ่ง (In Blossom OST) ทั้งหมดมีเพลงอะไรบ้าง' },
158
- { label: '🎬 ปมเปลี่ยนใบหน้า', query: 'อธิบายว่าปมเปลี่ยนใบหน้าในฮวาเจียนลิ่งส่งผลต่อการสืบคดีและตัวตนของหยางไฉ่เวยอย่างไร' }
159
- );
160
- } else if (combined.includes('หยุนซี') || combined.includes('yunxi') || combined.includes('芸汐传')) {
161
- suggestions.push(
162
- { label: '🌿 ทักษะพิษและวิชาแพทย์', query: 'เล่าจุดเด่นวิชาแพทย์และทักษะการใช้พิษของหานหยุนซีในตำนานหยุนซี' },
163
- { label: '🎵 เพลง 叹云兮 (Sigh of Yunxi)', query: 'เบื้องหลังและเสน่ห์ของเพลง 叹云兮 (Sigh of Yunxi) ขับร้องโดยจวีจิ้งอี' },
164
- { label: '🌟 เคมีคู่กับหลงเฟยเย่', query: 'เคมีการแสดงระหว่าง จวีจิ้งอี และ จางเจ๋อฮั่น ในซีรีส์ตำนานหยุนซี' }
165
- );
166
- } else if (combined.includes('นางพญางูขาว') || combined.includes('white snake') || combined.includes('ไป๋ซู่เจิน')) {
167
- suggestions.push(
168
- { label: '🐍 เสน่ห์บทบาทไป๋ซู่เจิน', query: 'ความโดดเด่นและชุดแต่งกายสีขาวอันเป็นเอกลักษณ์ของไป๋ซู่เจิน 2019' },
169
- { label: '🎵 เพลง 千年等一回', query: 'เพลง 千年等一回 เวอร์ชันจวีจิ้งอี มีความไพเราะและกระแสตอบรับอย่างไร' },
170
- { label: '✨ ฉากตำนานสะพานขาด', query: 'เล่าฉากพบกันบนสะพานขาดระหว่างไป๋ซู่เจินและสวี่เซียน' }
171
- );
172
- } else if (combined.includes('veil of shadows') || combined.includes('月鳞绮纪') || combined.includes('ลู่หวู่อี้')) {
173
- suggestions.push(
174
- { label: '🗡️ บทบาทลู่หวู่อี้ 2026', query: 'บทบาท ลู่หวู่อี้ ในซีรีส์ Veil of Shadows (月鳞绮纪) มีความท้าทายอย่างไร' },
175
- { label: '🎵 เพลงคู่ ไม่ทุกข์ตรม (不苦)', query: 'เบื้องหลังเพลง 不苦 (Not Suffering) ร้องคู่กับ เถียนเจียรุ่ย' },
176
- { label: '📺 กำหนดการฉายและแพลตฟอร์ม', query: 'อัปเดตสถานะและแพลตฟอร์มการออกอากาศของ Veil of Shadows' }
177
- );
178
- } else if (combined.includes('prsr') || combined.includes('pasha') || combined.includes('帕莎')) {
179
- suggestions.push(
180
- { label: '👓 PRSR Global Ambassador', query: 'เจาะลึกบทบาท Global Brand Ambassador ของ PRSR (Pasha) และลุคไวรัลใน Milan Fashion Week' },
181
- { label: '📺 คลิปโปรโมท PRSR บน Bilibili', query: 'ค้นหาคลิปโปรโมทและเบื้องหลังแว่นตาแฟชั่น PRSR บน Bilibili' },
182
- { label: '👗 แบรนด์พรีเซนเตอร์ทั้งหมด', query: 'สรุปแบรนด์ที่จวีจิ้งอีเป็นพรีเซนเตอร์และแอมบาสเดอร์ในปัจจุบัน' }
183
- );
184
- } else if (combined.includes('versace') || combined.includes('แบรนด์') || combined.includes('แฟชั่น') || combined.includes('พรีเซนเตอร์') || combined.includes('โปรโมท') || combined.includes('โปรโมต') || combined.includes('commercial')) {
185
- const brandTool = tools.find(t => t.name === 'get_brand_endorsements');
186
- const firstBrand = brandTool?.result?.brands?.[0];
187
- const brandTitle = firstBrand?.brand || 'Versace';
188
- suggestions.push(
189
- { label: `💎 ${brandTitle}`, query: `เจาะลึกบทบาท จวีจิ้งอี กับตำแหน่ง ${firstBrand?.title || 'Brand Ambassador'} ของ ${brandTitle}` },
190
- { label: '💄 สไตล์เมคอัพ Ju Jingyi', query: 'วิเคราะห์อิทธิพลของสไตล์การแต่งหน้า Ju Jingyi Makeup ที่ฮิตไปทั่วโลก' },
191
- { label: '👗 แบรนด์พรีเซนเตอร์ทั้งหมด', query: 'สรุปแบรนด์ที่จวีจิ้งอีเป็นพรีเซนเตอร์และแอมบาสเดอร์ในปัจจุบัน' }
192
- );
193
- } else if (combined.includes('เพลง') || combined.includes('song') || combined.includes('singles') || combined.includes('find the way') || combined.includes('everyday')) {
194
- suggestions.push(
195
- { label: '🚀 ซิงเกิล FIND THE WAY 2026', query: 'เบื้องหลังเพลง FIND THE WAY และคอนเซปต์ใหม่ปี 2026' },
196
- { label: '🎧 ซิงเกิลเดี่ยวและ OST ทั้งหมด', query: 'รวมเพลงเดี่ยวและเพลงประกอบซีรีส์ยอดนิยมของจวีจิ้งอี' },
197
- { label: '💃 สเตจเต้น 红昭愿 ในตำนาน', query: 'สเตจการเต้นเพลง 红昭愿 (Hong Zhao Yuan) ชุดแดงที่เป็นตำนาน' }
198
- );
752
+ if (offTopic) {
753
+ suggestions.push(...buildOffTopicFollowUps(query, detectedLang));
199
754
  } else {
755
+
756
+ const scoredTopics = [];
757
+ for (const candidate of TOPIC_CANDIDATES) {
758
+ let score = 0;
759
+ for (const kw of candidate.keywords) {
760
+ if (query.toLowerCase().includes(kw)) score += 12;
761
+ if (answer.toLowerCase().includes(kw)) score += 5;
762
+ }
763
+ if (candidate.toolMatcher && tools.some(candidate.toolMatcher)) {
764
+ score += 15;
765
+ }
766
+ if (score > 0) {
767
+ scoredTopics.push({ candidate, score });
768
+ }
769
+ }
770
+
771
+ scoredTopics.sort((a, b) => b.score - a.score);
772
+
773
+ if (scoredTopics.length > 0 && scoredTopics[0].score >= 10) {
774
+ const top = scoredTopics[0].candidate;
775
+ const topList = top.suggestions[detectedLang] || top.suggestions.th || top.suggestions.en || [];
200
776
 
201
- const workTool = tools.find(t => t.name === 'search_works' || t.name === 'get_work_detail');
202
- if (workTool && workTool.result?.works?.[0]) {
203
- const w = workTool.result.works[0];
204
- const title = w.titleTh || w.titleCn;
205
- suggestions.push(
206
- { label: `🎬 เจาะลึกบทบาทใน ${title}`, query: `เจาะลึกบทบาทและเบื้องหลังการแสดงของจวีจิ้งอีในเรื่อง ${title}` },
207
- { label: `🎵 เพลงประกอบซีรีส์ ${title}`, query: `เพลงประกอบซีรีส์ ${title} ขับร้องโดยใครบ้าง` },
208
- { label: `🏆 กระแสตอบรับของ ${title}`, query: `กระแสตอบรับ เรตติ้ง และรางวัลของซีรีส์ ${title}` }
209
- );
777
+ if (scoredTopics.length > 1 && scoredTopics[1].score >= 10) {
778
+
779
+ const second = scoredTopics[1].candidate;
780
+ const secondList = second.suggestions[detectedLang] || second.suggestions.th || second.suggestions.en || [];
781
+ suggestions.push(...topList.slice(0, 2));
782
+ if (secondList.length > 0) {
783
+ suggestions.push(secondList[0]);
784
+ }
785
+ } else {
786
+ suggestions.push(...topList);
787
+ }
210
788
  } else {
211
- suggestions.push(...GENERIC_FOLLOW_UP_POOL.slice(0, 3));
789
+ const workTool = tools.find(t => t.name === 'search_works' || t.name === 'get_work_detail');
790
+ if (workTool && workTool.result?.works?.[0]) {
791
+ const w = workTool.result.works[0];
792
+ const title = (detectedLang === 'zh' ? w.titleCn : (detectedLang === 'en' ? w.titleEn : (w.titleTh || w.titleCn))) || w.titleTh || w.titleCn;
793
+ if (detectedLang === 'zh') {
794
+ suggestions.push(
795
+ { label: `🎬 深入解析《${title}》`, query: `深入了解鞠婧祎在《${title}》中的角色塑造与幕后表演` },
796
+ { label: `🎵 《${title}》电视剧原声`, query: `电视剧《${title}》的原声音乐是由谁演唱的?` },
797
+ { label: `🏆 《${title}》播出反响与口碑`, query: `电视剧《${title}》的收视热度、观众口碑与荣誉` }
798
+ );
799
+ } else if (detectedLang === 'en') {
800
+ suggestions.push(
801
+ { label: `🎬 In-depth role in ${title}`, query: `Explore Ju Jingyi's acting performance and character depth in ${title}` },
802
+ { label: `🎵 Soundtracks of ${title}`, query: `What are the soundtrack songs from ${title} and who sang them?` },
803
+ { label: `🏆 Reception of ${title}`, query: `Ratings, audience reception, and awards for ${title}` }
804
+ );
805
+ } else {
806
+ suggestions.push(
807
+ { label: `🎬 เจาะลึกบทบาทใน ${title}`, query: `เจาะลึกบทบาทและเบื้องหลังการแสดงของจวีจิ้งอีในเรื่อง ${title}` },
808
+ { label: `🎵 เพลงประกอบซีรีส์ ${title}`, query: `เพลงประกอบซีรีส์ ${title} ขับร้องโดยใครบ้าง` },
809
+ { label: `🏆 กระแสตอบรับของ ${title}`, query: `กระแสตอบรับ เรตติ้ง และรางวัลของซีรีส์ ${title}` }
810
+ );
811
+ }
812
+ } else {
813
+ const brandTool = tools.find(t => t.name === 'get_brand_endorsements');
814
+ if (brandTool?.result?.brands?.[0]) {
815
+ const firstBrand = brandTool.result.brands[0];
816
+ const brandTitle = firstBrand.brand || 'Versace';
817
+ if (detectedLang === 'zh') {
818
+ suggestions.push(
819
+ { label: `💎 ${brandTitle} 品牌代言`, query: `深入了解鞠婧祎与 ${brandTitle} 的品牌合作与全球影响力` },
820
+ { label: '💄 鞠式美学出圈妆容', query: '解析风靡全网的「鞠式美学」与经典红毯出圈妆容' },
821
+ { label: '👗 商业代言汇总', query: '盘点鞠婧祎目前的全部合作品牌与商业价值' }
822
+ );
823
+ } else if (detectedLang === 'en') {
824
+ suggestions.push(
825
+ { label: `💎 ${brandTitle} Ambassadorship`, query: `Explore Ju Jingyi's role and campaign highlights with ${brandTitle}` },
826
+ { label: '💄 Signature Ju Jingyi Makeup', query: 'The viral influence and techniques behind Ju Jingyi Makeup' },
827
+ { label: '👗 Full Brand Endorsements', query: 'Summary of all current brand ambassadorships and commercial partnerships' }
828
+ );
829
+ } else {
830
+ suggestions.push(
831
+ { label: `💎 ${brandTitle}`, query: `เจาะลึกบทบาท จวีจิ้งอี กับตำแหน่ง ${firstBrand.title || 'Brand Ambassador'} ของ ${brandTitle}` },
832
+ { label: '💄 สไตล์เมคอัพ Ju Jingyi', query: 'วิเคราะห์อิทธิพลของสไตล์การแต่งหน้า Ju Jingyi Makeup ที่ฮิตไปทั่วโลก' },
833
+ { label: '👗 แบรนด์พรีเซนเตอร์ทั้งหมด', query: 'สรุปแบรนด์ที่จวีจิ้งอีเป็นพรีเซนเตอร์และแอมบาสเดอร์ในปัจจุบัน' }
834
+ );
835
+ }
836
+ } else {
837
+ const genericPool = GENERIC_FOLLOW_UP_POOLS[detectedLang] || GENERIC_FOLLOW_UP_POOLS.th;
838
+ suggestions.push(...genericPool.slice(0, 3));
839
+ }
840
+ }
212
841
  }
213
842
  }
214
843
 
844
+ if (offTopic) {
845
+ const offSeen = recentKeys instanceof Set ? recentKeys : new Set();
846
+ const fresh = dedupeSuggestions(suggestions, offSeen);
847
+ if (fresh.length >= 2) return fresh.slice(0, 3);
848
+ return dedupeSuggestions(suggestions, new Set()).slice(0, 3);
849
+ }
850
+
215
851
  const seen = recentKeys instanceof Set ? recentKeys : null;
216
852
  if (!seen) return suggestions.slice(0, 3);
217
853
 
218
- // Cross-turn freshness: drop questions already shown recently in this session.
219
854
  const fresh = dedupeSuggestions(suggestions, seen);
220
855
  if (fresh.length >= 2) return fresh.slice(0, 3);
221
856
 
222
- // The topic bucket itself was all recently shown — rotate through the generic
223
- // pool (cursor advances every call) so each turn proposes NEW questions.
857
+ const genericPool = GENERIC_FOLLOW_UP_POOLS[detectedLang] || GENERIC_FOLLOW_UP_POOLS.th;
224
858
  const rotated = [
225
- ...GENERIC_FOLLOW_UP_POOL.slice(followUpPoolCursor),
226
- ...GENERIC_FOLLOW_UP_POOL.slice(0, followUpPoolCursor),
859
+ ...genericPool.slice(followUpPoolCursor),
860
+ ...genericPool.slice(0, followUpPoolCursor),
227
861
  ];
228
- followUpPoolCursor = (followUpPoolCursor + 1) % GENERIC_FOLLOW_UP_POOL.length;
862
+ followUpPoolCursor = (followUpPoolCursor + 1) % genericPool.length;
229
863
  const freshGeneric = dedupeSuggestions(rotated, seen);
230
864
  const out = [...fresh, ...freshGeneric].slice(0, 3);
231
865
  if (out.length >= 2) return out;
232
866
 
233
- // Pool fully consumed for this session — better to recycle the least-recent
234
- // questions than to return nothing.
235
867
  return dedupeSuggestions(rotated, new Set()).slice(0, 3);
236
868
  }
237
869
 
238
- const GENERIC_FOLLOW_UP_POOL = Object.freeze([
239
- { label: '🎬 แนะนำซีรีส์ยอดนิยมที่ไม่ควรพลาด', query: 'แนะนำซีรีส์ยอดนิยมและบทบาทมาสเตอร์พีซของจวีจิ้งอี' },
240
- { label: '🎵 เพลงฮิตและสเตจแสดงสดเด่นๆ', query: 'รวมบทเพลงฮิตและสเตจการแสดงสดที่เป็นไวรัลของจวีจิ้งอี' },
241
- { label: '💎 ก้าวสู่ศิลปินเดี่ยวและรางวัล', query: 'เส้นทางการเป็นศิลปินเดี่ยว การก่อตั้งสตูดิโอ และรางวัลสำคัญ' },
242
- { label: '🏆 รางวัลและผลการประกวด', query: 'รางวัลและผลการประกวดสำคัญของจวีจิ้งอีตั้งแต่ยุค SNH48' },
243
- { label: '📅 ไทม์ไลน์อาชีพ 2013-2026', query: 'ไทม์ไลน์อาชีพของจวีจิ้งอีตั้งแต่เดบิวต์ SNH48 จนถึงยุคศิลปินเดี่ยว' },
244
- { label: '🎤 สเตจไวรัลที่ต้องดู', query: 'สเตจการแสดงสดของจวีจิ้งอีที่กลายเป็นไวรัลมากที่สุด' },
245
- { label: '📸 แฟชั่นและลุคเด่น', query: 'ลุคแฟชั่นและงานพรีเซนเตอร์แบรนด์ที่โดดเด่นของจวีจิ้งอี' },
246
- { label: '📺 ผลงานซีรีส์ล่าสุด', query: 'ผลงานซีรีส์ล่าสุดและโปรเจกต์ที่กำลังจะมาของจวีจิ้งอี' },
247
- { label: '🎬 ฉากไวรัลในซีรีส์', query: 'ฉากที่ไวรัลที่สุดในซีรีส์ที่จวีจิ้งอีแสดง' },
248
- { label: '🌟 การแสดงที่ได้รับคำชม', query: 'การแสดงของจวีจิ้งอีที่ได้รับคำชมจากนักวิจารณ์และผู้ชม' },
249
- { label: '🎹 บทเพลงที่ขับร้องเอง', query: 'รวมบทเพลงที่จวีจิ้งอีขับร้องเองทั้งหมด' },
250
- { label: '🌏 แฟนคลับสากล', query: 'แฟนคลับสากลและชื่อเสียงระดับโลกของจวีจิ้งอี' },
251
- { label: '🧧 อีเวนต์และกำหนดการ', query: 'กำหนดการงานเทศกาลและอีเวนต์ของจวีจิ้งอี' },
252
- { label: '🌺 ยุค SNH48 ก่อนเดี่ยว', query: 'ช่วงเวลาสำคัญของจวีจิ้งอีในยุค SNH48 ก่อนออกมาเดี่ยว' },
253
- ]);
870
+ export const GENERIC_FOLLOW_UP_POOLS = Object.freeze({
871
+ th: [
872
+ { label: '🎬 แนะนำซีรีส์ยอดนิยมที่ไม่ควรพลาด', query: 'แนะนำซีรีส์ยอดนิยมและบทบาทมาสเตอร์พีซของจวีจิ้งอี' },
873
+ { label: '🎵 เพลงฮิตและสเตจแสดงสดเด่นๆ', query: 'รวมบทเพลงฮิตและสเตจการแสดงสดที่เป็นไวรัลของจวีจิ้งอี' },
874
+ { label: '💎 ก้าวสู่ศิลปินเดี่ยวและรางวัล', query: 'เส้นทางการเป็นศิลปินเดี่ยว การก่อตั้งสตูดิโอ และรางวัลสำคัญ' },
875
+ { label: '🏆 รางวัลและผลการประกวด', query: 'รางวัลและผลการประกวดสำคัญของจวีจิ้งอีตั้งแต่ยุค SNH48' },
876
+ { label: '📅 ไทม์ไลน์อาชีพ 2013-2026', query: 'ไทม์ไลน์อาชีพของจวีจิ้งอีตั้งแต่เดบิวต์ SNH48 จนถึงยุคศิลปินเดี่ยว' },
877
+ { label: '🎤 สเตจไวรัลที่ต้องดู', query: 'สเตจการแสดงสดของจวีจิ้งอีที่กลายเป็นไวรัลมากที่สุด' },
878
+ { label: '📸 แฟชั่นและลุคเด่น', query: 'ลุคแฟชั่นและงานพรีเซนเตอร์แบรนด์ที่โดดเด่นของจวีจิ้งอี' },
879
+ { label: '📺 ผลงานซีรีส์ล่าสุด', query: 'ผลงานซีรีส์ล่าสุดและโปรเจกต์ที่กำลังจะมาของจวีจิ้งอี' },
880
+ { label: '🎬 ฉากไวรัลในซีรีส์', query: 'ฉากที่ไวรัลที่สุดในซีรีส์ที่จวีจิ้งอีแสดง' },
881
+ { label: '🌟 การแสดงที่ได้รับคำชม', query: 'การแสดงของจวีจิ้งอีที่ได้รับคำชมจากนักวิจารณ์และผู้ชม' },
882
+ { label: '🎹 บทเพลงที่ขับร้องเอง', query: 'รวมบทเพลงที่จวีจิ้งอีขับร้องเองทั้งหมด' },
883
+ { label: '🌏 แฟนคลับสากล', query: 'แฟนคลับสากลและชื่อเสียงระดับโลกของจวีจิ้งอี' },
884
+ { label: '🧧 อีเวนต์และกำหนดการ', query: 'กำหนดการงานเทศกาลและอีเวนต์ของจวีจิ้งอี' },
885
+ { label: '🌺 ยุค SNH48 ก่อนเดี่ยว', query: 'ช่วงเวลาสำคัญของจวีจิ้งอีในยุค SNH48 ก่อนออกมาเดี่ยว' },
886
+ ],
887
+ zh: [
888
+ { label: '🎬 经典影视代表作盘点', query: '推荐鞠婧祎最受好评的经典影视剧与代表角色' },
889
+ { label: '🎵 热门单曲与神级舞台', query: '盘点鞠婧祎最具代表性的个人单曲与出圈舞台表演' },
890
+ { label: '💎 个人工作室与商业价值', query: '鞠婧祎独立发展、成立个人工作室及顶级品牌合作' },
891
+ { label: '🏆 历年荣誉与奖项记录', query: '总结鞠婧祎在影视和音乐领域斩获的主要奖项与成就' },
892
+ { label: '📅 演艺生涯发展历程', query: '鞠婧祎从女团出道到全能艺人的演艺事业发展脉络' },
893
+ { label: '📸 经典红毯与美学妆容', query: '盘点鞠婧祎惊艳全网的红毯名场面与标志性妆容' },
894
+ ],
895
+ en: [
896
+ { label: '🎬 Most Iconic Drama Series', query: 'What are Ju Jingyi\'s most acclaimed and popular drama series?' },
897
+ { label: '🎵 Signature Songs & Hit Stages', query: 'What are Ju Jingyi\'s most famous solo songs and viral stage performances?' },
898
+ { label: '💎 Independent Studio & Brands', query: 'Ju Jingyi\'s transition to solo artist, independent studio, and luxury brand ambassadorships' },
899
+ { label: '🏆 Major Awards & Milestones', query: 'Summary of major acting, music, and popularity awards won by Ju Jingyi' },
900
+ { label: '📅 Career Timeline & Evolution', query: 'Explore Ju Jingyi\'s career milestones from debut to present' },
901
+ { label: '📸 Viral Fashion & Signature Makeup', query: 'The viral influence of Ju Jingyi\'s red carpet looks and signature makeup aesthetics' },
902
+ ]
903
+ });
904
+
905
+ export const GENERIC_FOLLOW_UP_POOL = GENERIC_FOLLOW_UP_POOLS.th;
254
906
  let followUpPoolCursor = 0;
255
907
 
256
908
  export default {
257
909
  extractAgentFollowUpQuestions,
258
910
  generateDynamicFollowUps,
911
+ isOffTopicChat,
912
+ buildOffTopicFollowUps,
259
913
  };