@tencent-rtc/trtc-agent-skills 0.1.3 → 0.1.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 (360) hide show
  1. package/.cursor/rules/main.mdc +12 -0
  2. package/AGENTS.md +14 -104
  3. package/CLAUDE.md +15 -127
  4. package/CODEBUDDY.md +32 -118
  5. package/README.md +7 -5
  6. package/README.zh.md +7 -5
  7. package/bin/cli.js +133 -41
  8. package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
  9. package/hooks/cursor-adapter.py +45 -18
  10. package/hooks/hooks-cursor.json +5 -18
  11. package/hooks/hooks.json +6 -31
  12. package/knowledge-base/conference/web/index.yaml +143 -0
  13. package/knowledge-base/platform-slice-template.md +1041 -133
  14. package/knowledge-base/products.yaml +76 -0
  15. package/knowledge-base/scenario-spec.md +1316 -115
  16. package/knowledge-base/scenarios/conference/base/general-conference.md +22 -2
  17. package/knowledge-base/scenarios/conference/base/webinar-conference.md +14 -0
  18. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +19 -0
  19. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +13 -0
  20. package/knowledge-base/scenarios/live/entertainment-live-room.md +12 -0
  21. package/knowledge-base/slice-spec.md +2377 -299
  22. package/knowledge-base/slices/conference/web/integration-audit.md +1 -1
  23. package/knowledge-base/slices/conference/web/login-auth.md +14 -1
  24. package/knowledge-base/tooling/aliases.yaml +92 -0
  25. package/knowledge-base/tooling/intent-signals.yaml +181 -0
  26. package/knowledge-base/tooling/symptom-keywords.yaml +21 -0
  27. package/package.json +1 -1
  28. package/skills/trtc/SKILL.md +202 -245
  29. package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
  30. package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_read.py +12 -8
  31. package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_write.py +16 -12
  32. package/skills/{trtc-topic/guardrails → trtc/hooks}/stop_require_apply_evidence.py +22 -14
  33. package/skills/trtc/hooks/topic_phase_gate.py +161 -0
  34. package/skills/{trtc-topic → trtc}/runtime/README.md +2 -2
  35. package/skills/{trtc-onboarding/reference/reporting-protocol.md → trtc/runtime/REPORTING.md} +5 -5
  36. package/skills/{trtc-topic → trtc}/runtime/RUNTIME.md +5 -5
  37. package/skills/trtc/runtime/lib/__init__.py +0 -0
  38. package/skills/{trtc-topic → trtc}/runtime/package-lock.json +2 -2
  39. package/skills/{trtc-topic → trtc}/runtime/package.json +2 -2
  40. package/skills/{trtc-topic → trtc}/runtime/telemetry-bridge.mjs +1 -1
  41. package/skills/{trtc-topic/scripts → trtc/tools}/STATE-MACHINE-GUIDE.md +23 -23
  42. package/skills/trtc/tools/__init__.py +2 -0
  43. package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  44. package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
  45. package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
  46. package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
  47. package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
  48. package/skills/trtc/tools/apply.py +540 -0
  49. package/skills/trtc/tools/docs.py +712 -0
  50. package/skills/trtc/tools/docsbot.py +182 -0
  51. package/skills/trtc/tools/entry/render_ai_instructions.py +92 -0
  52. package/skills/trtc/tools/flow.py +1089 -0
  53. package/skills/{trtc-topic/scripts → trtc/tools}/init_slice_queue.py +5 -4
  54. package/skills/{trtc-topic/scripts → trtc/tools}/next_slice.py +5 -4
  55. package/skills/trtc/tools/query_classifier.py +301 -0
  56. package/skills/trtc/tools/reporting.py +447 -0
  57. package/skills/trtc/tools/search.py +817 -0
  58. package/skills/trtc/tools/session.py +1261 -0
  59. package/skills/trtc/tools/state_machine.py +690 -0
  60. package/skills/trtc-ai-service/README.md +195 -0
  61. package/skills/trtc-ai-service/README.zh-CN.md +193 -0
  62. package/skills/trtc-ai-service/SKILL.md +945 -0
  63. package/skills/trtc-ai-service/auto_adapters/README.md +40 -0
  64. package/skills/trtc-ai-service/auto_adapters/frontend-spa/README.md +27 -0
  65. package/skills/trtc-ai-service/auto_adapters/frontend-spa/angular/voice-agent.component.ts.tpl +131 -0
  66. package/skills/trtc-ai-service/auto_adapters/frontend-spa/manifest.yaml +57 -0
  67. package/skills/trtc-ai-service/auto_adapters/frontend-spa/react/VoiceAgent.tsx.tpl +142 -0
  68. package/skills/trtc-ai-service/auto_adapters/frontend-spa/vue/VoiceAgent.vue.tpl +121 -0
  69. package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-backend.md +45 -0
  70. package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-frontend.md +51 -0
  71. package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-rest-api.md +93 -0
  72. package/skills/trtc-ai-service/auto_adapters/java-backend/README.md +25 -0
  73. package/skills/trtc-ai-service/auto_adapters/java-backend/manifest.yaml +30 -0
  74. package/skills/trtc-ai-service/auto_adapters/java-backend/quarkus/VoiceAgentFilter.java.tpl +64 -0
  75. package/skills/trtc-ai-service/auto_adapters/java-backend/springboot/VoiceAgentFilter.java.tpl +91 -0
  76. package/skills/trtc-ai-service/auto_adapters/manifest.yaml +43 -0
  77. package/skills/trtc-ai-service/auto_adapters/node-backend/README.md +25 -0
  78. package/skills/trtc-ai-service/auto_adapters/node-backend/express.js.tpl +40 -0
  79. package/skills/trtc-ai-service/auto_adapters/node-backend/fastify.js.tpl +27 -0
  80. package/skills/trtc-ai-service/auto_adapters/node-backend/koa.js.tpl +31 -0
  81. package/skills/trtc-ai-service/auto_adapters/node-backend/manifest.yaml +47 -0
  82. package/skills/trtc-ai-service/auto_adapters/python-backend/README.md +22 -0
  83. package/skills/trtc-ai-service/auto_adapters/python-backend/django.py.tpl +32 -0
  84. package/skills/trtc-ai-service/auto_adapters/python-backend/fastapi.py.tpl +35 -0
  85. package/skills/trtc-ai-service/auto_adapters/python-backend/flask.py.tpl +31 -0
  86. package/skills/trtc-ai-service/auto_adapters/python-backend/manifest.yaml +45 -0
  87. package/skills/trtc-ai-service/capabilities/__init__.py +43 -0
  88. package/skills/trtc-ai-service/capabilities/conversation-core/.env.example +29 -0
  89. package/skills/trtc-ai-service/capabilities/conversation-core/INTEGRATION.md +134 -0
  90. package/skills/trtc-ai-service/capabilities/conversation-core/INTERFACE_ADAPT.md +111 -0
  91. package/skills/trtc-ai-service/capabilities/conversation-core/QUICK_START.md +62 -0
  92. package/skills/trtc-ai-service/capabilities/conversation-core/manifest.yaml +250 -0
  93. package/skills/trtc-ai-service/capabilities/conversation-core/requirements.txt +6 -0
  94. package/skills/trtc-ai-service/capabilities/conversation-core/src/__init__.py +10 -0
  95. package/skills/trtc-ai-service/capabilities/conversation-core/src/_capability_loader.py +218 -0
  96. package/skills/trtc-ai-service/capabilities/conversation-core/src/agent.py +231 -0
  97. package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +132 -0
  98. package/skills/trtc-ai-service/capabilities/conversation-core/src/health.py +355 -0
  99. package/skills/trtc-ai-service/capabilities/conversation-core/src/log_filter.py +76 -0
  100. package/skills/trtc-ai-service/capabilities/conversation-core/src/modality.py +109 -0
  101. package/skills/trtc-ai-service/capabilities/conversation-core/src/server.py +312 -0
  102. package/skills/trtc-ai-service/capabilities/conversation-core/src/trtc_client.py +315 -0
  103. package/skills/trtc-ai-service/capabilities/conversation-core/src/usersig.py +90 -0
  104. package/skills/trtc-ai-service/capabilities/conversation-core/tests/test_skeleton.py +216 -0
  105. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/README.md +48 -0
  106. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/app.js +415 -0
  107. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/index.html +68 -0
  108. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/styles.css +136 -0
  109. package/skills/trtc-ai-service/capabilities/digital-human/README.md +31 -0
  110. package/skills/trtc-ai-service/capabilities/digital-human/manifest.yaml +64 -0
  111. package/skills/trtc-ai-service/capabilities/digital-human/src/__init__.py +2 -0
  112. package/skills/trtc-ai-service/capabilities/digital-human/src/router.py +43 -0
  113. package/skills/trtc-ai-service/capabilities/human-handoff/INTERFACE_ADAPT.md +353 -0
  114. package/skills/trtc-ai-service/capabilities/human-handoff/README.md +44 -0
  115. package/skills/trtc-ai-service/capabilities/human-handoff/manifest.yaml +227 -0
  116. package/skills/trtc-ai-service/capabilities/human-handoff/src/__init__.py +2 -0
  117. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/__init__.py +9 -0
  118. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/default_rest.py +242 -0
  119. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/factory.py +89 -0
  120. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/local_queue.py +258 -0
  121. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/mock.py +132 -0
  122. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/__init__.py +25 -0
  123. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/intent_detector.py +75 -0
  124. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/models.py +163 -0
  125. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/service.py +192 -0
  126. package/skills/trtc-ai-service/capabilities/human-handoff/src/feedback_store.py +54 -0
  127. package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/__init__.py +4 -0
  128. package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/handoff_client.py +86 -0
  129. package/skills/trtc-ai-service/capabilities/human-handoff/src/queue.py +62 -0
  130. package/skills/trtc-ai-service/capabilities/human-handoff/src/router.py +201 -0
  131. package/skills/trtc-ai-service/capabilities/human-handoff/src/summary_link.py +77 -0
  132. package/skills/trtc-ai-service/capabilities/human-handoff/src/trigger.py +25 -0
  133. package/skills/trtc-ai-service/capabilities/knowledge-base/INTERFACE_ADAPT.md +297 -0
  134. package/skills/trtc-ai-service/capabilities/knowledge-base/README.md +51 -0
  135. package/skills/trtc-ai-service/capabilities/knowledge-base/data/faq.json +20 -0
  136. package/skills/trtc-ai-service/capabilities/knowledge-base/manifest.yaml +211 -0
  137. package/skills/trtc-ai-service/capabilities/knowledge-base/src/__init__.py +8 -0
  138. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/__init__.py +9 -0
  139. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/default_rest.py +209 -0
  140. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/factory.py +86 -0
  141. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/local_json.py +172 -0
  142. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/mock.py +91 -0
  143. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/__init__.py +12 -0
  144. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/models.py +77 -0
  145. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/scoring.py +73 -0
  146. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/service.py +78 -0
  147. package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/__init__.py +4 -0
  148. package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/kb_client.py +61 -0
  149. package/skills/trtc-ai-service/capabilities/knowledge-base/src/retriever.py +56 -0
  150. package/skills/trtc-ai-service/capabilities/knowledge-base/src/router.py +85 -0
  151. package/skills/trtc-ai-service/capabilities/session-summary/INTERFACE_ADAPT.md +99 -0
  152. package/skills/trtc-ai-service/capabilities/session-summary/README.md +47 -0
  153. package/skills/trtc-ai-service/capabilities/session-summary/data/test_session.json +18 -0
  154. package/skills/trtc-ai-service/capabilities/session-summary/manifest.yaml +165 -0
  155. package/skills/trtc-ai-service/capabilities/session-summary/src/__init__.py +2 -0
  156. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/__init__.py +5 -0
  157. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/base.py +31 -0
  158. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/default_rest.py +67 -0
  159. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/factory.py +51 -0
  160. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/local_json.py +42 -0
  161. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/mock.py +22 -0
  162. package/skills/trtc-ai-service/capabilities/session-summary/src/recorder.py +210 -0
  163. package/skills/trtc-ai-service/capabilities/session-summary/src/router.py +93 -0
  164. package/skills/trtc-ai-service/capabilities/session-summary/src/summarizer.py +163 -0
  165. package/skills/trtc-ai-service/capabilities/tool-calling/INTERFACE_ADAPT.md +158 -0
  166. package/skills/trtc-ai-service/capabilities/tool-calling/README.md +50 -0
  167. package/skills/trtc-ai-service/capabilities/tool-calling/data/tools.yaml +58 -0
  168. package/skills/trtc-ai-service/capabilities/tool-calling/examples/__init__.py +1 -0
  169. package/skills/trtc-ai-service/capabilities/tool-calling/examples/local_tools.py +101 -0
  170. package/skills/trtc-ai-service/capabilities/tool-calling/manifest.yaml +146 -0
  171. package/skills/trtc-ai-service/capabilities/tool-calling/src/__init__.py +8 -0
  172. package/skills/trtc-ai-service/capabilities/tool-calling/src/dispatcher.py +54 -0
  173. package/skills/trtc-ai-service/capabilities/tool-calling/src/registry.py +219 -0
  174. package/skills/trtc-ai-service/capabilities/tool-calling/src/router.py +50 -0
  175. package/skills/trtc-ai-service/references/business-contract-spec.md +263 -0
  176. package/skills/trtc-ai-service/scenarios/custom-builder/README.md +86 -0
  177. package/skills/trtc-ai-service/scenarios/custom-builder/output-templates/recipe.yaml.j2 +194 -0
  178. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q1-business-scenario.md +43 -0
  179. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q2-io-modality.md +57 -0
  180. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q3-ui-form.md +55 -0
  181. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q4-capabilities.md +78 -0
  182. package/skills/trtc-ai-service/scenarios/customer-service/README.md +114 -0
  183. package/skills/trtc-ai-service/scenarios/customer-service/recipe.yaml +154 -0
  184. package/skills/trtc-ai-service/scenarios/customer-service/sample-data/README.md +32 -0
  185. package/skills/trtc-ai-service/scenarios/customer-service/sample-data/faq-sample.json +37 -0
  186. package/skills/trtc-ai-service/scenarios/customer-service/system-prompt.template.md +94 -0
  187. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/app.js +347 -0
  188. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/index.html +125 -0
  189. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/styles.css +487 -0
  190. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/tokens.css +71 -0
  191. package/skills/trtc-ai-service/scenarios/customer-service/ui/design-system/DESIGN_GUIDELINES.md +370 -0
  192. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/README.md +68 -0
  193. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/app.js +1307 -0
  194. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/data.js +40 -0
  195. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/index.html +233 -0
  196. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/mock-shop.json +21 -0
  197. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/styles.css +603 -0
  198. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/tokens.css +71 -0
  199. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/agent-link.js +323 -0
  200. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/app.js +458 -0
  201. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/index.html +109 -0
  202. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/styles.css +489 -0
  203. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/tokens.css +59 -0
  204. package/skills/trtc-ai-service/scripts/add-capability.py +364 -0
  205. package/skills/trtc-ai-service/scripts/contract-adapt.py +334 -0
  206. package/skills/trtc-ai-service/scripts/detect-stack.py +40 -0
  207. package/skills/trtc-ai-service/scripts/lib/__init__.py +20 -0
  208. package/skills/trtc-ai-service/scripts/lib/adapter_codegen.py +509 -0
  209. package/skills/trtc-ai-service/scripts/lib/arbitrator.py +152 -0
  210. package/skills/trtc-ai-service/scripts/lib/contract_resolver.py +519 -0
  211. package/skills/trtc-ai-service/scripts/lib/credential_validators.py +253 -0
  212. package/skills/trtc-ai-service/scripts/lib/curl_parser.py +303 -0
  213. package/skills/trtc-ai-service/scripts/lib/degrader.py +140 -0
  214. package/skills/trtc-ai-service/scripts/lib/injector.py +347 -0
  215. package/skills/trtc-ai-service/scripts/lib/manifest_resolver.py +288 -0
  216. package/skills/trtc-ai-service/scripts/lib/openapi_parser.py +289 -0
  217. package/skills/trtc-ai-service/scripts/lib/stack_detector.py +159 -0
  218. package/skills/trtc-ai-service/scripts/lib/tokens_compile.py +204 -0
  219. package/skills/trtc-ai-service/scripts/post-install-patch.py +225 -0
  220. package/skills/trtc-ai-service/scripts/setup-credentials.py +393 -0
  221. package/skills/trtc-ai-service/scripts/verify-credentials.py +108 -0
  222. package/skills/trtc-ai-service/start.sh +111 -0
  223. package/skills/trtc-ai-service/tests/__init__.py +1 -0
  224. package/skills/trtc-ai-service/tests/test_arbitrator.py +64 -0
  225. package/skills/trtc-ai-service/tests/test_capability_overlay.py +85 -0
  226. package/skills/trtc-ai-service/tests/test_contract_resolver.py +190 -0
  227. package/skills/trtc-ai-service/tests/test_handoff_ports.py +195 -0
  228. package/skills/trtc-ai-service/tests/test_kb_ports.py +195 -0
  229. package/skills/trtc-ai-service/tests/test_manifest_resolver.py +95 -0
  230. package/skills/trtc-ai-service/tests/test_recipe_assembly.py +175 -0
  231. package/skills/trtc-ai-service/tests/test_stack_and_degrader.py +101 -0
  232. package/skills/trtc-ai-service/tests/test_verify_credentials.py +285 -0
  233. package/skills/trtc-ai-service/triggers.yaml +29 -0
  234. package/skills/trtc-conference/SKILL.md +324 -0
  235. package/skills/trtc-conference/flows/onboarding.md +205 -0
  236. package/skills/trtc-conference/flows/topic.md +474 -0
  237. package/skills/trtc-conference/flows/troubleshoot.md +85 -0
  238. package/skills/trtc-conference/hooks/pretooluse_require_business_decisions.py +213 -0
  239. package/skills/trtc-conference/playbooks/medical-quickstart.md +84 -0
  240. package/skills/trtc-conference/playbooks/official-roomkit.md +97 -0
  241. package/skills/trtc-conference/references/local-usersig/basic-info-config.ts +39 -0
  242. package/skills/trtc-conference/references/usersig-handling.md +134 -0
  243. package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  244. package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  245. package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
  246. package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
  247. package/skills/trtc-conference/tests/test_conference_index_contract.py +43 -0
  248. package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +103 -0
  249. package/skills/trtc-conference/tests/test_conference_template_contract.py +25 -0
  250. package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +132 -0
  251. package/skills/trtc-conference/tools/apply_checks.py +328 -0
  252. package/skills/trtc-conference/verify_lib/__init__.py +0 -0
  253. package/skills/{trtc-apply/guardrails/apply_lib → trtc-conference/verify_lib}/rule_parser.py +1 -1
  254. package/skills/trtc-docs/SKILL.md +91 -119
  255. package/.cursor/rules/ui-mode.mdc +0 -99
  256. package/ai-instructions/base.md +0 -13
  257. package/ai-instructions/ui-mode.md +0 -93
  258. package/knowledge-base/index.yaml +0 -462
  259. package/skills/trtc/room-builder/SKILL.md +0 -133
  260. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +0 -108
  261. package/skills/trtc/room-builder/tools/render_ai_instructions.py +0 -226
  262. package/skills/trtc-apply/SKILL.md +0 -97
  263. package/skills/trtc-onboarding/SKILL.md +0 -841
  264. package/skills/trtc-onboarding/reference/path-a1-demo.md +0 -103
  265. package/skills/trtc-onboarding/reference/path-a2-integrate.md +0 -737
  266. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +0 -186
  267. package/skills/trtc-onboarding/reference/path-c-expand.md +0 -43
  268. package/skills/trtc-onboarding/reference/supported-matrix.md +0 -100
  269. package/skills/trtc-search/SKILL.md +0 -228
  270. package/skills/trtc-topic/SKILL.md +0 -622
  271. package/skills/trtc-topic/scripts/apply.py +0 -581
  272. package/skills/trtc-topic/scripts/lib/state_machine.py +0 -328
  273. package/skills/trtc-topic/tests/README.md +0 -70
  274. package/skills/trtc-topic/tests/conftest.py +0 -72
  275. package/skills/trtc-topic/tests/test_apply_cli.py +0 -480
  276. package/skills/trtc-topic/tests/test_end_to_end.py +0 -305
  277. package/skills/trtc-topic/tests/test_finalize_session.py +0 -51
  278. package/skills/trtc-topic/tests/test_gates.py +0 -316
  279. package/skills/trtc-topic/tests/test_session_resolver.py +0 -260
  280. package/skills/trtc-topic/tests/test_state_machine.py +0 -414
  281. package/skills/trtc-topic/tests/test_stop_require_apply.py +0 -99
  282. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +0 -130
  283. /package/skills/{trtc-topic → trtc}/runtime/lib/platforms.py +0 -0
  284. /package/skills/{trtc-topic → trtc}/runtime/telemetry_collector.py +0 -0
  285. /package/skills/{trtc-onboarding/reference → trtc/runtime}/usersig-handling.md +0 -0
  286. /package/skills/{trtc-topic/scripts → trtc/tools}/finalize_session.py +0 -0
  287. /package/skills/{trtc-apply/guardrails/apply_lib → trtc-ai-service/capabilities/conversation-core/tests}/__init__.py +0 -0
  288. /package/skills/{trtc-topic → trtc-conference}/references/execution-units.yaml +0 -0
  289. /package/skills/{trtc/room-builder/templates/scenarios/medical-consultation/src/config → trtc-conference/references/local-usersig}/lib-generate-test-usersig-es.min.d.ts +0 -0
  290. /package/skills/{trtc/room-builder/templates/scenarios/medical-consultation/src/config → trtc-conference/references/local-usersig}/lib-generate-test-usersig-es.min.js +0 -0
  291. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/backend-contract.zh-CN.md +0 -0
  292. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/integration.zh-CN.md +0 -0
  293. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/theme.zh-CN.md +0 -0
  294. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/index.html +0 -0
  295. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/package.json +0 -0
  296. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/postcss.config.js +0 -0
  297. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/App.vue +0 -0
  298. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/ConsultationManagePanel.vue +0 -0
  299. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LanguageSwitch.vue +0 -0
  300. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LoadingSpinner.vue +0 -0
  301. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalAlert.vue +0 -0
  302. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalBusinessPanel.vue +0 -0
  303. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalButton.vue +0 -0
  304. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalConfirmDialog.vue +0 -0
  305. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalDataPanel.vue +0 -0
  306. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalRecordPanel.vue +0 -0
  307. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/PrescriptionPanel.vue +0 -0
  308. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/basic-info-config.ts +0 -0
  309. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/runtime-config.ts +0 -0
  310. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/env.d.ts +0 -0
  311. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +0 -0
  312. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +0 -0
  313. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +0 -0
  314. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +0 -0
  315. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +0 -0
  316. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +0 -0
  317. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/types.ts +0 -0
  318. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationChat.ts +0 -0
  319. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationDevices.ts +0 -0
  320. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationParticipants.ts +0 -0
  321. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationPermissions.ts +0 -0
  322. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/utils.ts +0 -0
  323. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/en-US/index.ts +0 -0
  324. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/index.ts +0 -0
  325. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/medicalTranslate.ts +0 -0
  326. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/state.ts +0 -0
  327. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/zh-CN/index.ts +0 -0
  328. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/main.ts +0 -0
  329. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/appointments.ts +0 -0
  330. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/users.ts +0 -0
  331. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/router/index.ts +0 -0
  332. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/index.ts +0 -0
  333. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/appointmentService.ts +0 -0
  334. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/authService.ts +0 -0
  335. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/launchContext.ts +0 -0
  336. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/userService.ts +0 -0
  337. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/appointmentService.ts +0 -0
  338. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/authService.ts +0 -0
  339. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/userService.ts +0 -0
  340. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/types.ts +0 -0
  341. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/shared/icons.ts +0 -0
  342. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/index.css +0 -0
  343. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/tailwind.css +0 -0
  344. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/theme.css +0 -0
  345. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/auth.ts +0 -0
  346. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/format.ts +0 -0
  347. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/navigation.ts +0 -0
  348. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/session.ts +0 -0
  349. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorConsultationView.vue +0 -0
  350. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorDashboardView.vue +0 -0
  351. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/LoginView.vue +0 -0
  352. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationFinishedView.vue +0 -0
  353. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationView.vue +0 -0
  354. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientSelectDoctorView.vue +0 -0
  355. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientWaitingView.vue +0 -0
  356. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.json +0 -0
  357. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.node.json +0 -0
  358. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/vite.config.ts +0 -0
  359. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md" +0 -0
  360. /package/skills/{trtc-topic/runtime/lib → trtc-conference/tools}/__init__.py +0 -0
@@ -0,0 +1,1307 @@
1
+ /* ===== Voice AI Customer Service · Main Application =====
2
+ *
3
+ * Connects to the TRTC Conversational AI backend to implement:
4
+ * 1. Click Start → get_config → TRTC enterRoom → agent/start
5
+ * 2. Left-side cards → send text to AI → TTS reply
6
+ * 3. Human Support → handoff/request → queue animation → simulated connection
7
+ *
8
+ * TRTC custom message protocol (aligned with conversation-core):
9
+ * Send (client → bot): cmdId=2, data is JSON encoded as ArrayBuffer via TextEncoder
10
+ * type=20000 text injection, type=20001 interrupt
11
+ * Receive (bot → client): JSON decoded via TextDecoder
12
+ * type=10000 subtitle (payload.text/end/roundid), type=10001 state (payload.state)
13
+ */
14
+ (function() {
15
+ 'use strict';
16
+
17
+ // =====================================================================
18
+ // STATE
19
+ // =====================================================================
20
+ var state = {
21
+ active: false,
22
+ healthy: false,
23
+ mode: 'pre', // pre / idle / listening / speaking / thinking
24
+ sessionId: null,
25
+ sdkAppId: 0,
26
+ roomId: null,
27
+ userId: null,
28
+ userSig: null,
29
+ agentUserId: null,
30
+ trtcClient: null,
31
+ micEnabled: false,
32
+ handoffActive: false,
33
+ handoffConnected: false, // true once a human agent is connected (AI stopped)
34
+ ending: false, // true while the farewell is being spoken before hangup
35
+ queueTimer: null,
36
+ queueSeconds: 0,
37
+ localEchoes: [], // [{ text, at: timestamp }] for dedup
38
+ aiRoundText: {}, // roundid → accumulated text
39
+ aiRoundLast: {}, // roundid → last chunk
40
+ aiRoundBubbleId: {}, // roundid → DOM element id (not used in chat drawer)
41
+ aiRoundBubbleEl: {}, // roundid → DOM element (bubble node in chat) for in-place update
42
+ userBubbleOpen: false,
43
+ transcript: [], // [{ role, text }] mirror of the session for summary upload
44
+ lastSessionId: null, // remembered after hangup so the rating card can post feedback
45
+ ratingValue: 0, // currently selected star value in the rating card
46
+ };
47
+
48
+ // Handoff keywords (fast-path; if not matched, the frontend still asks the
49
+ // backend /api/v1/handoff/detect to reuse the same intent_detector logic)
50
+ var HANDOFF_KEYWORDS = [
51
+ 'talk to agent', 'human agent', 'human support', 'real person',
52
+ 'live agent', 'speak to a human', 'transfer to agent', 'real human',
53
+ ];
54
+
55
+ // Weak signals that make an utterance worth a backend /detect round-trip.
56
+ // Used as a guard so ordinary "help me with my order" requests are NOT sent to
57
+ // the (broad) intent_detector and mis-transferred to a human.
58
+ var HANDOFF_HINT_WORDS = [
59
+ 'human', 'agent', 'person', 'representative', 'someone real',
60
+ 'live chat', 'manager', '人工', '客服', '真人', '坐席',
61
+ ];
62
+
63
+ // End-of-conversation keywords (issue 1 / issue 5): voice-triggered farewell + auto hangup
64
+ var END_KEYWORDS = [
65
+ 'bye', 'goodbye', 'end conversation', 'end the call', 'end this call',
66
+ '结束', '再见', '拜拜', '挂断', '结束对话',
67
+ ];
68
+
69
+ var ECHO_TTL_MS = 30000;
70
+ var FAREWELL_HANGUP_DELAY_MS = 4200; // let the farewell TTS finish before hanging up
71
+
72
+ // =====================================================================
73
+ // API helpers
74
+ // =====================================================================
75
+ function api(method, path, body) {
76
+ var opts = { method: method, credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } };
77
+ if (body) opts.body = JSON.stringify(body);
78
+ return fetch(path, opts).then(function(resp) {
79
+ return resp.json().then(function(data) {
80
+ if (!resp.ok) {
81
+ var msg = (data && (data.detail && data.detail.message || data.detail || data.msg)) || resp.statusText;
82
+ throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
83
+ }
84
+ return data;
85
+ });
86
+ });
87
+ }
88
+
89
+ // =====================================================================
90
+ // DOM helpers
91
+ // =====================================================================
92
+ function $(id) { return document.getElementById(id); }
93
+
94
+ function uuid() {
95
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
96
+ var r = (Math.random() * 16) | 0;
97
+ var v = c === 'x' ? r : (r & 0x3) | 0x8;
98
+ return v.toString(16);
99
+ });
100
+ }
101
+
102
+ // =====================================================================
103
+ // Connection status
104
+ // =====================================================================
105
+ function setConnected(on) {
106
+ var dot = $('conn-dot');
107
+ var txt = $('conn-text');
108
+ var wrap = $('conn-status');
109
+ if (!wrap) return;
110
+ wrap.dataset.connected = on ? 'true' : 'false';
111
+ if (on) {
112
+ dot.style.background = '#34c77b';
113
+ txt.textContent = 'Live · Connected';
114
+ } else {
115
+ dot.style.background = '#c9c2d8';
116
+ txt.textContent = 'Disconnected';
117
+ }
118
+ }
119
+
120
+ // =====================================================================
121
+ // Mode / visual state
122
+ // =====================================================================
123
+ function setMode(m) {
124
+ state.mode = m;
125
+ var orb = document.querySelector('.orb-wrap');
126
+ var wave = $('wave');
127
+ var aiState = $('ai-state');
128
+ var aiSub = $('ai-substate');
129
+
130
+ if (orb) {
131
+ orb.classList.remove('idle', 'listening', 'speaking');
132
+ }
133
+ if (wave) {
134
+ wave.classList.remove('idle', 'listening', 'speaking');
135
+ }
136
+
137
+ switch (m) {
138
+ case 'pre':
139
+ if (orb) orb.classList.add('idle');
140
+ if (wave) wave.classList.add('idle');
141
+ if (aiState) aiState.textContent = 'Ready to start conversation';
142
+ if (aiSub) aiSub.innerHTML = 'Press <span class="kbd">Start</span> below to begin a real-time voice session';
143
+ break;
144
+ case 'idle':
145
+ if (orb) orb.classList.add('idle');
146
+ if (wave) wave.classList.add('idle');
147
+ if (aiState) aiState.textContent = 'Listening for you';
148
+ if (aiSub) aiSub.textContent = 'Speak naturally · I will respond in real time';
149
+ break;
150
+ case 'listening':
151
+ if (orb) orb.classList.add('listening');
152
+ if (wave) wave.classList.add('listening');
153
+ if (aiState) aiState.textContent = 'Listening\u2026';
154
+ if (aiSub) aiSub.textContent = 'Capturing your voice';
155
+ break;
156
+ case 'thinking':
157
+ if (orb) orb.classList.add('listening');
158
+ if (wave) wave.classList.add('listening');
159
+ if (aiState) aiState.textContent = 'Thinking\u2026';
160
+ if (aiSub) aiSub.textContent = 'Processing your request';
161
+ break;
162
+ case 'speaking':
163
+ if (orb) orb.classList.add('speaking');
164
+ if (wave) wave.classList.add('speaking');
165
+ if (aiState) aiState.textContent = 'AI is speaking\u2026';
166
+ if (aiSub) aiSub.textContent = 'Streaming response over TRTC';
167
+ break;
168
+ }
169
+ }
170
+
171
+ // =====================================================================
172
+ // Chat drawer bubbles
173
+ // =====================================================================
174
+ function openDrawer() {
175
+ $('im-drawer').classList.add('open');
176
+ }
177
+
178
+ function addBubble(role, text) {
179
+ var chat = $('chat');
180
+ if (role === 'ai') text = stripMarkdown(text);
181
+ var div;
182
+ if (role === 'system') {
183
+ div = document.createElement('div');
184
+ div.className = 'flex justify-center';
185
+ div.innerHTML = '<div class="bubble system">' + escapeHtml(text) + '</div>';
186
+ chat.appendChild(div);
187
+ } else {
188
+ var row = document.createElement('div');
189
+ row.className = 'bubble-row ' + role;
190
+ var avatar = role === 'ai'
191
+ ? '<div class="bubble-avatar ai">AI</div>'
192
+ : '<div class="bubble-avatar user">U</div>';
193
+ var inner = '<div class="bubble ' + role + '">' + escapeHtml(text) + '</div>';
194
+ row.innerHTML = role === 'user' ? inner + avatar : avatar + inner;
195
+ chat.appendChild(row);
196
+ }
197
+ chat.scrollTop = chat.scrollHeight;
198
+ }
199
+
200
+ function addTyping() {
201
+ var chat = $('chat');
202
+ var row = document.createElement('div');
203
+ row.className = 'bubble-row ai';
204
+ row.id = 'typing-row';
205
+ row.innerHTML = '<div class="bubble-avatar ai">AI</div><div class="bubble ai"><div class="typing"><span></span><span></span><span></span></div></div>';
206
+ chat.appendChild(row);
207
+ chat.scrollTop = chat.scrollHeight;
208
+ }
209
+
210
+ function removeTyping() {
211
+ var r = $('typing-row');
212
+ if (r) r.remove();
213
+ }
214
+
215
+ function escapeHtml(s) {
216
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
217
+ }
218
+
219
+ // Display-layer markdown sanitizer (defense-in-depth only).
220
+ // NOTE: this strips markdown symbols from the on-screen transcript; it CANNOT affect
221
+ // what TTS reads aloud (TTS runs server-side from raw LLM output). The real fix for
222
+ // "TTS reads * out loud" is the backend default instructions that forbid markdown.
223
+ function stripMarkdown(s) {
224
+ if (!s) return '';
225
+ return String(s)
226
+ .replace(/```[\s\S]*?```/g, ' ') // fenced code blocks
227
+ .replace(/`([^`]*)`/g, '$1') // inline code
228
+ .replace(/\*\*([^*]+)\*\*/g, '$1') // bold **x**
229
+ .replace(/\*([^*]+)\*/g, '$1') // italic *x*
230
+ .replace(/__([^_]+)__/g, '$1') // bold __x__
231
+ .replace(/_([^_]+)_/g, '$1') // italic _x_
232
+ .replace(/~~([^~]+)~~/g, '$1') // strikethrough
233
+ .replace(/^\s{0,3}#{1,6}\s+/gm, '') // ATX headers
234
+ .replace(/^\s*[-*+]\s+/gm, '') // bullet list markers
235
+ .replace(/^\s*\d+\.\s+/gm, '') // numbered list markers
236
+ .replace(/[*_`~#]/g, '') // any leftover symbols
237
+ .replace(/[ \t]{2,}/g, ' ')
238
+ .trim();
239
+ }
240
+
241
+ // =====================================================================
242
+ // Local echo dedup
243
+ // =====================================================================
244
+ function pushLocalEcho(text) {
245
+ var now = Date.now();
246
+ state.localEchoes = state.localEchoes.filter(function(e) { return now - e.at < ECHO_TTL_MS; });
247
+ state.localEchoes.push({ text: (text || '').trim(), at: now });
248
+ }
249
+
250
+ function consumeLocalEcho(text) {
251
+ var tt = (text || '').trim();
252
+ if (!tt) return false;
253
+ var now = Date.now();
254
+ state.localEchoes = state.localEchoes.filter(function(e) { return now - e.at < ECHO_TTL_MS; });
255
+ for (var i = 0; i < state.localEchoes.length; i++) {
256
+ var e = state.localEchoes[i];
257
+ var a = e.text.toLowerCase();
258
+ var b = tt.toLowerCase();
259
+ if (e.text === tt || a.indexOf(b) !== -1 || b.indexOf(a) !== -1) {
260
+ state.localEchoes.splice(i, 1);
261
+ return true;
262
+ }
263
+ }
264
+ return false;
265
+ }
266
+
267
+ // =====================================================================
268
+ // TRTC custom message: send (binary-encoded JSON)
269
+ // =====================================================================
270
+ function sendCustomToAgent(message) {
271
+ if (!state.trtcClient) return false;
272
+ try {
273
+ state.trtcClient.sendCustomMessage({
274
+ cmdId: 2,
275
+ data: new TextEncoder().encode(JSON.stringify(message)).buffer,
276
+ });
277
+ return true;
278
+ } catch (e) {
279
+ console.warn('sendCustomMessage failed', e);
280
+ return false;
281
+ }
282
+ }
283
+
284
+ function sendInterrupt() {
285
+ return sendCustomToAgent({
286
+ type: 20001,
287
+ sender: state.userId,
288
+ receiver: [state.agentUserId],
289
+ payload: { id: uuid(), timestamp: Date.now() },
290
+ });
291
+ }
292
+
293
+ // =====================================================================
294
+ // TRTC custom message: receive handler
295
+ // =====================================================================
296
+ function handleCustomMessage(event) {
297
+ var txt;
298
+ try {
299
+ txt = new TextDecoder().decode(event.data);
300
+ } catch (e) {
301
+ console.warn('decode custom msg failed', e);
302
+ return;
303
+ }
304
+ var data;
305
+ try {
306
+ data = JSON.parse(txt);
307
+ } catch (e) {
308
+ console.warn('parse custom msg failed', e);
309
+ return;
310
+ }
311
+
312
+ if (!data || typeof data !== 'object') return;
313
+
314
+ // type=10001: AI state change (payload.state)
315
+ if (data.type === 10001) {
316
+ var code = data.payload && data.payload.state;
317
+ switch (code) {
318
+ case 1: setMode('listening'); break;
319
+ case 2: setMode('thinking'); break;
320
+ case 3: setMode('speaking'); break;
321
+ case 4: setMode('idle'); break;
322
+ default: break;
323
+ }
324
+ return;
325
+ }
326
+
327
+ // type=10000: subtitle / caption
328
+ if (data.type !== 10000) return;
329
+
330
+ var payload = data.payload || {};
331
+ var text = payload.text || '';
332
+ var end = payload.end === true;
333
+ var roundid = payload.roundid || '';
334
+ var sender = data.sender || event.userId || '';
335
+ var isUser = sender === state.userId;
336
+
337
+ if (isUser) {
338
+ // User utterance echoed back by bot
339
+ if (end && text.trim()) {
340
+ var tt = text.trim();
341
+ // System-level injection (card clicks) — suppress from chat, only forward to AI
342
+ if (tt.indexOf('[system]') === 0) {
343
+ onUserUtteranceFinal(tt);
344
+ return;
345
+ }
346
+ if (consumeLocalEcho(tt)) return; // dedup
347
+ if (!state.userBubbleOpen) {
348
+ addBubble('user', tt);
349
+ state.userBubbleOpen = true;
350
+ }
351
+ onUserUtteranceFinal(tt);
352
+ setTimeout(function() { state.userBubbleOpen = false; }, 1500);
353
+ }
354
+ return;
355
+ }
356
+
357
+ // AI caption: incremental, aggregated by roundid — update a SINGLE bubble
358
+ if (text.trim() && roundid) {
359
+ removeTyping();
360
+ var last = state.aiRoundLast[roundid] || '';
361
+ var cur = state.aiRoundText[roundid] || '';
362
+ var isAccumulative = last && text.indexOf(last) === 0;
363
+ state.aiRoundText[roundid] = isAccumulative ? text : (cur + text);
364
+ state.aiRoundLast[roundid] = text;
365
+
366
+ // Update existing bubble or create a new one for this round
367
+ var existingEl = state.aiRoundBubbleEl[roundid];
368
+ if (existingEl && existingEl.parentNode) {
369
+ // In-place update of the existing bubble text
370
+ existingEl.textContent = stripMarkdown(state.aiRoundText[roundid]);
371
+ } else {
372
+ // First chunk of a new round: create a new bubble and track it
373
+ var row = document.createElement('div');
374
+ row.className = 'bubble-row ai';
375
+ row.innerHTML = '<div class="bubble-avatar ai">AI</div><div class="bubble ai">' + escapeHtml(stripMarkdown(state.aiRoundText[roundid])) + '</div>';
376
+ var chat = $('chat');
377
+ chat.appendChild(row);
378
+ chat.scrollTop = chat.scrollHeight;
379
+ // Track the inner .bubble element (not the row) for text updates
380
+ state.aiRoundBubbleEl[roundid] = row.querySelector('.bubble.ai');
381
+ }
382
+ }
383
+
384
+ if (end && roundid) {
385
+ // Record the completed assistant turn for later summary upload (issue 2)
386
+ var aiFinalText = state.aiRoundText[roundid];
387
+ if (aiFinalText) recordTurn('assistant', aiFinalText);
388
+ delete state.aiRoundText[roundid];
389
+ delete state.aiRoundLast[roundid];
390
+ delete state.aiRoundBubbleEl[roundid];
391
+ }
392
+ }
393
+
394
+ // =====================================================================
395
+ // Start: get_config → TRTC enterRoom → agent/start
396
+ // =====================================================================
397
+ function start() {
398
+ if (state.active) return;
399
+
400
+ setMode('thinking');
401
+ addBubble('system', 'Initializing connection\u2026');
402
+ $('btn-start').disabled = true;
403
+
404
+ api('POST', '/api/v1/get_config', {})
405
+ .then(function(cfg) {
406
+ // Response is wrapped: { code, msg, data: { session_id, sdk_app_id, ... } }
407
+ var d = cfg.data || cfg;
408
+ state.sessionId = d.session_id;
409
+ state.sdkAppId = d.sdk_app_id;
410
+ state.roomId = parseInt(d.room_id, 10);
411
+ state.userId = d.user_id;
412
+ state.userSig = d.user_sig;
413
+ state.agentUserId = d.agent_user_id;
414
+
415
+ console.log('[start] config:', JSON.stringify({
416
+ sessionId: state.sessionId,
417
+ sdkAppId: state.sdkAppId,
418
+ roomId: state.roomId,
419
+ userId: state.userId,
420
+ agentUserId: state.agentUserId,
421
+ }));
422
+
423
+ if (typeof TRTC === 'undefined') throw new Error('TRTC Web SDK not loaded');
424
+
425
+ state.trtcClient = TRTC.create();
426
+ state.trtcClient.on(TRTC.EVENT.CUSTOM_MESSAGE, handleCustomMessage);
427
+ state.trtcClient.on(TRTC.EVENT.ERROR, function(err) {
428
+ console.error('[trtc] error', err);
429
+ addBubble('system', 'TRTC error. Please try restarting.');
430
+ });
431
+
432
+ return state.trtcClient.enterRoom({
433
+ roomId: state.roomId,
434
+ scene: 'rtc',
435
+ sdkAppId: state.sdkAppId,
436
+ userId: state.userId,
437
+ userSig: state.userSig,
438
+ });
439
+ })
440
+ .then(function() {
441
+ // Start local audio — unmuted to capture user voice
442
+ return state.trtcClient.startLocalAudio().then(function() {
443
+ state.micEnabled = true;
444
+ updateMicUI();
445
+ }).catch(function(e) {
446
+ console.warn('mic init failed (continuing in text-only mode):', e);
447
+ state.micEnabled = false;
448
+ });
449
+ })
450
+ .then(function() {
451
+ return api('POST', '/api/v1/agent/start', {
452
+ session_id: state.sessionId,
453
+ language: 'en',
454
+ });
455
+ })
456
+ .then(function() {
457
+ state.active = true;
458
+ setConnected(true);
459
+
460
+ // Expand dock
461
+ var dock = $('dock');
462
+ if (dock) dock.dataset.state = 'expanded';
463
+
464
+ // Show Open chat button
465
+ var chatBtn = $('ctl-chat');
466
+ if (chatBtn) chatBtn.style.display = 'inline-flex';
467
+
468
+ setMode('idle');
469
+ addBubble('system', 'Connected. Click the microphone to talk, or type below.');
470
+ if (window.AppData && window.AppData.greeting) {
471
+ addBubble('ai', window.AppData.greeting);
472
+ }
473
+ })
474
+ .catch(function(err) {
475
+ console.error('[start] error', err);
476
+ addBubble('system', 'Failed to connect: ' + (err.message || err));
477
+ setMode('pre');
478
+ setConnected(false);
479
+ $('btn-start').disabled = false;
480
+ safeExitRoom();
481
+ });
482
+ }
483
+
484
+ // =====================================================================
485
+ // Hangup
486
+ // =====================================================================
487
+ function safeExitRoom() {
488
+ var p = Promise.resolve();
489
+ if (state.trtcClient) {
490
+ p = state.trtcClient.exitRoom().catch(function() {}).then(function() {
491
+ if (state.trtcClient) { state.trtcClient.destroy(); state.trtcClient = null; }
492
+ });
493
+ }
494
+ return p;
495
+ }
496
+
497
+ function hangupCleanup() {
498
+ if (state.queueTimer) {
499
+ clearInterval(state.queueTimer);
500
+ state.queueTimer = null;
501
+ }
502
+ stopQueueAnimation();
503
+ state.active = false;
504
+ state.handoffActive = false;
505
+ state.handoffConnected = false;
506
+ state.ending = false;
507
+ state.sessionId = null;
508
+ state.transcript = [];
509
+ state.localEchoes = [];
510
+ state.aiRoundText = {};
511
+ state.aiRoundLast = {};
512
+ state.aiRoundBubbleId = {};
513
+ state.aiRoundBubbleEl = {};
514
+
515
+ var rc = $('right-console');
516
+ if (rc) rc.classList.remove('handoff-mode');
517
+ setHangButtonMode(false);
518
+ var dock = $('dock');
519
+ if (dock) dock.dataset.state = 'collapsed';
520
+ var chatBtn = $('ctl-chat');
521
+ if (chatBtn) chatBtn.style.display = 'none';
522
+ $('btn-start').disabled = false;
523
+
524
+ if (state.micEnabled) {
525
+ state.micEnabled = false;
526
+ updateMicUI();
527
+ }
528
+ }
529
+
530
+ function hangup() {
531
+ addBubble('system', 'Ending session\u2026');
532
+
533
+ // Remember the session so the post-call rating card can submit feedback (issue 6)
534
+ var ratingSessionId = state.sessionId;
535
+
536
+ var stopPromise = Promise.resolve();
537
+ if (state.sessionId) {
538
+ stopPromise = api('POST', '/api/v1/agent/stop', { session_id: state.sessionId }).catch(function() {});
539
+ }
540
+
541
+ stopPromise.then(function() {
542
+ return safeExitRoom();
543
+ }).then(function() {
544
+ setConnected(false);
545
+ hangupCleanup();
546
+ addBubble('system', 'Conversation ended.');
547
+ setMode('pre');
548
+ closeDetailViewSilent();
549
+ // Issue 6: pop the CSAT rating card after a real session
550
+ if (ratingSessionId) {
551
+ state.lastSessionId = ratingSessionId;
552
+ showRatingCard(ratingSessionId);
553
+ }
554
+ });
555
+ }
556
+
557
+ // =====================================================================
558
+ // Send text to AI
559
+ // =====================================================================
560
+ function sendText(text) {
561
+ text = (text || '').trim();
562
+ if (!text) return;
563
+
564
+ openDrawer();
565
+ if (!state.active || !state.trtcClient) {
566
+ addBubble('system', 'Please start the conversation first.');
567
+ return;
568
+ }
569
+
570
+ // Farewell in progress — block input
571
+ if (state.ending) {
572
+ addBubble('system', 'The conversation is ending. Please wait.');
573
+ return;
574
+ }
575
+ // Human agent connected — block input from reaching the LLM (issue 4)
576
+ if (state.handoffConnected) {
577
+ addBubble('system', 'You are now connected to a human agent. Please speak naturally.');
578
+ return;
579
+ }
580
+ // During handoff queue, block user messages from reaching LLM
581
+ if (state.handoffActive) {
582
+ addBubble('system', 'Please wait while we connect you to an agent.');
583
+ return;
584
+ }
585
+
586
+ addBubble('user', text);
587
+ pushLocalEcho(text);
588
+ sendInterrupt();
589
+
590
+ setTimeout(function() {
591
+ sendCustomToAgent({
592
+ type: 20000,
593
+ sender: state.userId,
594
+ receiver: [state.agentUserId],
595
+ payload: { id: uuid(), message: text, timestamp: Date.now() },
596
+ });
597
+ }, 120);
598
+
599
+ onUserUtteranceFinal(text);
600
+ }
601
+
602
+ function onUserUtteranceFinal(text) {
603
+ // [system] injections (card clicks) are authoritative data fed TO the AI, not real
604
+ // user speech — never run end/handoff intent detection on them. (Their wording, e.g.
605
+ // "do NOT transfer to a human agent", would otherwise falsely trigger handoff.)
606
+ if (text && String(text).indexOf('[system]') === 0) return;
607
+
608
+ // Mirror the user turn for later summary upload (skips [system] injections)
609
+ recordTurn('user', text);
610
+
611
+ // Once a human agent is connected, the AI pipeline is stopped — ignore everything
612
+ if (state.handoffConnected) return;
613
+ // While ending (farewell playing) or in the handoff queue, block LLM/TTS
614
+ if (state.ending || state.handoffActive) return;
615
+
616
+ // Issue 5: voice-triggered end intent → play farewell → auto hangup (issue 1 exit)
617
+ if (detectEndIntent(text)) {
618
+ endConversationWithFarewell();
619
+ return;
620
+ }
621
+
622
+ // Issue 7: handoff — keyword fast-path first, then ask backend /detect (reuses the
623
+ // same intent_detector logic) so voice phrases not in the local list still match.
624
+ var lower = text.toLowerCase();
625
+ if (HANDOFF_KEYWORDS.some(function(kw) { return lower.indexOf(kw) !== -1; })) {
626
+ talkToAgent();
627
+ return;
628
+ }
629
+ // Fallback to backend /detect ONLY when the utterance shows a human/agent signal.
630
+ // The backend intent_detector also matches broad words like "help"/"support", so
631
+ // calling it on every message would mis-transfer normal help requests; this guard
632
+ // keeps the AI-helps flow intact while still catching "let me talk to a representative".
633
+ var hasHandoffSignal = HANDOFF_HINT_WORDS.some(function(w) { return lower.indexOf(w) !== -1; });
634
+ if (hasHandoffSignal) {
635
+ api('POST', '/api/v1/handoff/detect', { text: text })
636
+ .then(function(resp) {
637
+ if (resp && resp.data && resp.data.matched) talkToAgent();
638
+ })
639
+ .catch(function(e) { console.debug('[detect] error', e); });
640
+ }
641
+
642
+ // KB lookup silently
643
+ silentKbLookup(text).catch(function(e) { console.debug('[kb] error', e); });
644
+ }
645
+
646
+ // Detect end-of-conversation intent from user speech/text (issue 5)
647
+ function detectEndIntent(text) {
648
+ if (!text) return false;
649
+ var lower = String(text).toLowerCase();
650
+ return END_KEYWORDS.some(function(kw) { return lower.indexOf(kw) !== -1; });
651
+ }
652
+
653
+ // Mirror a conversation turn for later summary upload (issue 2).
654
+ // [system] injections (card clicks) are NOT real user speech — skip them.
655
+ function recordTurn(role, text) {
656
+ if (!text) return;
657
+ var t = String(text).trim();
658
+ if (!t) return;
659
+ if (t.indexOf('[system]') === 0) return;
660
+ state.transcript.push({ role: role, text: t });
661
+ }
662
+
663
+ // Issue 1 + 2 + 5: play a configurable farewell over TTS, then auto-hangup.
664
+ // Used by BOTH the voice-triggered end intent and the manual hang-up button, so the
665
+ // AI always speaks the farewell before the call closes (and before the rating card).
666
+ function endConversationWithFarewell() {
667
+ if (state.ending) return;
668
+ state.ending = true;
669
+ // If a handoff is mid-flight (queue animation + preset broadcasts), cancel it so its
670
+ // pending timers stop injecting more announcements over the farewell.
671
+ if (state.handoffActive) {
672
+ state.handoffActive = false;
673
+ stopQueueAnimation();
674
+ }
675
+ // Mute the mic immediately so ambient noise cannot keep feeding the AI new "user"
676
+ // messages during the farewell — otherwise the AI stays listening and the call can't
677
+ // close cleanly (issue: manual hang-up blocked by background noise).
678
+ if (state.trtcClient) {
679
+ state.trtcClient.updateLocalAudio({ mute: true }).catch(function() {});
680
+ state.micEnabled = false;
681
+ updateMicUI();
682
+ }
683
+ addBubble('system', 'Ending conversation\u2026');
684
+ setMode('speaking');
685
+
686
+ var farewell = (window.AppData && window.AppData.farewell) ||
687
+ 'Thank you for contacting us. Have a wonderful day. Goodbye!';
688
+ sendInterrupt(); // cut any ongoing AI response before the farewell
689
+ speakFixed(farewell);
690
+
691
+ // Wait long enough for the TTS to actually finish speaking the farewell before
692
+ // stopping the agent. Estimate from word count (TTS ~3 words/sec + synthesis lead).
693
+ var words = String(farewell).split(/\s+/).filter(Boolean).length;
694
+ var delay = Math.max(FAREWELL_HANGUP_DELAY_MS, Math.round(words * 380 + 2500));
695
+ setTimeout(function() {
696
+ if (!state.ending) return;
697
+ state.ending = false;
698
+ hangup();
699
+ }, delay);
700
+ }
701
+
702
+ // Decide how to hang up based on the current state (issue 2):
703
+ // - human-agent mode / already ending → hang up directly (AI is stopped, no TTS farewell)
704
+ // - AI active → play the farewell first, then hang up
705
+ function requestHangup() {
706
+ if (state.ending) { hangup(); return; }
707
+ if (state.handoffConnected) { hangup(); return; }
708
+ if (state.active) { endConversationWithFarewell(); return; }
709
+ hangup();
710
+ }
711
+
712
+ // Issue 4: switch the UI to the human-agent state and stop the AI pipeline.
713
+ function enterHumanAgentMode() {
714
+ state.handoffConnected = true;
715
+ state.handoffActive = false;
716
+ var rc = $('right-console');
717
+ if (rc) rc.classList.add('handoff-mode');
718
+ // Calm the orb into a gentle "listening" animation (AI is stopped, no fast breathe)
719
+ var orb = document.querySelector('.orb-wrap');
720
+ var wave = $('wave');
721
+ if (orb) { orb.classList.remove('idle', 'listening', 'speaking'); orb.classList.add('listening'); }
722
+ if (wave) { wave.classList.remove('idle', 'listening', 'speaking'); wave.classList.add('listening'); }
723
+ var aiState = $('ai-state');
724
+ var aiSub = $('ai-substate');
725
+ if (aiState) aiState.textContent = 'Connected to a human agent';
726
+ if (aiSub) aiSub.textContent = 'You are now speaking with a live agent · AI assistant paused';
727
+ // Issue 4: in human-agent mode the dock shows ONLY an "End call" button (mic +
728
+ // human-support are hidden via CSS .handoff-mode). Restyle the hang buttons.
729
+ setHangButtonMode(true);
730
+ }
731
+
732
+ // Toggle the hang-up button between its normal (icon-only circle) and human-agent
733
+ // (pill with "End call" label) appearance.
734
+ function setHangButtonMode(on) {
735
+ [$('btn-hang'), $('btn-hang-detail')].forEach(function(b) {
736
+ if (!b) return;
737
+ if (on) {
738
+ b.classList.add('hang-agent');
739
+ b.innerHTML = '<i data-lucide="phone-off" class="w-5 h-5"></i><span>End call</span>';
740
+ } else {
741
+ b.classList.remove('hang-agent');
742
+ b.innerHTML = '<i data-lucide="phone-off" class="w-5 h-5"></i>';
743
+ }
744
+ });
745
+ if (window.lucide) lucide.createIcons();
746
+ }
747
+
748
+ // Speak a PRESET line verbatim over TTS. The line is wrapped in a [system] directive
749
+ // so the LLM echoes it EXACTLY instead of composing its own reply to the announcement
750
+ // (a bare type-20000 injection is treated as user input and the LLM paraphrases it).
751
+ // Used for the fixed handoff announcements and the farewell so the AI says exactly the
752
+ // scripted text. Does not sendInterrupt — callers decide when to cut prior audio.
753
+ function speakFixed(text) {
754
+ pushLocalEcho(text);
755
+ addBubble('ai', text);
756
+ var msg = '[system] This is a scripted announcement. Speak to the user EXACTLY the ' +
757
+ 'following sentence, verbatim, with no preamble, no paraphrase, and no extra words. ' +
758
+ 'Output only this sentence and nothing else: ' + text;
759
+ setTimeout(function() {
760
+ sendCustomToAgent({
761
+ type: 20000,
762
+ sender: state.userId,
763
+ receiver: [state.agentUserId],
764
+ payload: { id: uuid(), message: msg, timestamp: Date.now() },
765
+ });
766
+ }, 120);
767
+ }
768
+
769
+ function silentKbLookup(query) {
770
+ return api('POST', '/api/v1/kb/search', { query: query, top_k: 1 })
771
+ .then(function(resp) {
772
+ var hits = resp && resp.data ? resp.data : (Array.isArray(resp) ? resp : []);
773
+ if (!hits.length) return;
774
+ var top = hits[0];
775
+ if (typeof top.score === 'number' && top.score < 0.15) return;
776
+ console.debug('[kb hit]', top.entry && top.entry.id, 'score=', top.score);
777
+ });
778
+ }
779
+
780
+ // =====================================================================
781
+ // Mic toggle
782
+ // =====================================================================
783
+ function updateMicUI() {
784
+ var btn = $('btn-mic');
785
+ var btnD = $('btn-mic-detail');
786
+ [btn, btnD].forEach(function(b) {
787
+ if (!b) return;
788
+ b.classList.toggle('muted', !state.micEnabled);
789
+ b.innerHTML = '<i data-lucide="' + (state.micEnabled ? 'mic' : 'mic-off') + '" class="w-5 h-5"></i>';
790
+ });
791
+ if (window.lucide) lucide.createIcons();
792
+ }
793
+
794
+ function toggleMute() {
795
+ if (!state.trtcClient) return;
796
+ state.micEnabled = !state.micEnabled;
797
+ state.trtcClient.updateLocalAudio({ mute: !state.micEnabled }).catch(function(e) {
798
+ console.error('toggleMic failed', e);
799
+ state.micEnabled = !state.micEnabled;
800
+ });
801
+ updateMicUI();
802
+ if (state.micEnabled) sendInterrupt();
803
+ }
804
+
805
+ // =====================================================================
806
+ // Human support (handoff)
807
+ // =====================================================================
808
+ function talkToAgent() {
809
+ if (!state.active || !state.sessionId) {
810
+ addBubble('system', 'Please start the conversation before requesting an agent.');
811
+ return;
812
+ }
813
+ if (state.handoffConnected) {
814
+ addBubble('system', 'You are already connected to a human agent.');
815
+ return;
816
+ }
817
+ if (state.handoffActive) {
818
+ addBubble('system', 'You are already in the handoff queue. Please wait.');
819
+ return;
820
+ }
821
+ if (state.ending) {
822
+ addBubble('system', 'The conversation is ending. Please wait.');
823
+ return;
824
+ }
825
+
826
+ state.handoffActive = true;
827
+
828
+ // Issue 3: mute the mic during the queue broadcast so the TTS guidance is not
829
+ // interrupted by user audio picked up by the microphone.
830
+ if (state.trtcClient) {
831
+ state.trtcClient.updateLocalAudio({ mute: true }).catch(function(e) {
832
+ console.warn('mute-on-handoff failed', e);
833
+ });
834
+ state.micEnabled = false;
835
+ updateMicUI();
836
+ }
837
+
838
+ addBubble('system', 'Transferring to a human agent\u2026');
839
+ setMode('speaking');
840
+
841
+ // Show queue animation
842
+ startQueueAnimation();
843
+
844
+ var sid = state.sessionId;
845
+
846
+ // Issue 2: upload the conversation transcript BEFORE requesting the handoff, so
847
+ // the backend can attach a context summary to the ticket (attach_summary_to_ticket
848
+ // reads from the session-summary recorder). Must precede /handoff/request.
849
+ api('POST', '/api/v1/summary/' + encodeURIComponent(sid) + '/record', {
850
+ turns: state.transcript.slice(),
851
+ }).catch(function(e) { console.debug('[summary record] error', e); });
852
+
853
+ // Then create the handoff ticket (carry the last user utterance as the reason)
854
+ var reason = (state.transcript.length && state.transcript[state.transcript.length - 1].text) || 'human handoff';
855
+ api('POST', '/api/v1/handoff/request', { session_id: sid, reason: reason }).catch(function() {});
856
+
857
+ // Fixed handoff voice flow (identical whether voice-triggered or button-clicked).
858
+ // The queue animation runs 8s. Two preset lines are spoken verbatim via [system]
859
+ // directives (speakFixed) so the AI reads them exactly, not an LLM paraphrase:
860
+ // t=2s → "Connecting you to a human agent. Please hold." (正在为您转接人工客服,请稍候)
861
+ // t=6s → "You are now connected." (已接通; 2s before animation ends)
862
+ // t=8s → animation done → switch UI to human mode + connect ticket
863
+ // t=10s → stop the AI (after "You are now connected." finishes, so it isn't cut)
864
+ setTimeout(function() {
865
+ if (!state.handoffActive) return;
866
+ sendInterrupt(); // cut any ongoing AI response before the first preset line
867
+ speakFixed("Connecting you to a human agent. Please hold.");
868
+ setMode('speaking');
869
+ }, 2000);
870
+
871
+ setTimeout(function() {
872
+ if (!state.handoffActive) return;
873
+ speakFixed("You are now connected.");
874
+ }, 6000);
875
+
876
+ setTimeout(function() {
877
+ if (!state.handoffActive) return;
878
+ stopQueueAnimation();
879
+ api('POST', '/api/v1/handoff/connect', { session_id: sid }).catch(function() {});
880
+ addBubble('system', 'Agent connected');
881
+ enterHumanAgentMode();
882
+ }, 8000);
883
+
884
+ setTimeout(function() {
885
+ if (!state.handoffConnected) return;
886
+ api('POST', '/api/v1/agent/stop', { session_id: sid }).catch(function() {});
887
+ }, 10000);
888
+ }
889
+
890
+ function startQueueAnimation() {
891
+ var wrap = $('queue-progress-wrap');
892
+ var wave = $('wave');
893
+ var bar = $('queue-bar');
894
+ var timer = $('queue-timer');
895
+ if (!wrap) return;
896
+
897
+ // Hide wave visualizer, show progress bar in its place
898
+ if (wave) wave.style.display = 'none';
899
+ wrap.classList.add('show-progress');
900
+
901
+ // Reset bar to 0% and trigger 0→100% transition once
902
+ if (bar) {
903
+ bar.style.transition = 'none';
904
+ bar.style.width = '0%';
905
+ // Force reflow, then start the 8s linear fill
906
+ void bar.offsetWidth;
907
+ bar.style.transition = 'width 8000ms linear';
908
+ bar.style.width = '100%';
909
+ }
910
+
911
+ state.queueSeconds = 0;
912
+ if (timer) timer.textContent = '0:00';
913
+
914
+ // Timer ticks — counters only, don't touch the bar
915
+ state.queueTimer = setInterval(function() {
916
+ state.queueSeconds++;
917
+ var t = $('queue-timer');
918
+ if (t) {
919
+ var m = Math.floor(state.queueSeconds / 60);
920
+ var s = state.queueSeconds % 60;
921
+ t.textContent = m + ':' + (s < 10 ? '0' : '') + s;
922
+ }
923
+ if (state.queueSeconds >= 8 && state.queueTimer) {
924
+ clearInterval(state.queueTimer);
925
+ state.queueTimer = null;
926
+ }
927
+ }, 1000);
928
+ }
929
+
930
+ function stopQueueAnimation() {
931
+ if (state.queueTimer) {
932
+ clearInterval(state.queueTimer);
933
+ state.queueTimer = null;
934
+ }
935
+ var wrap = $('queue-progress-wrap');
936
+ if (wrap) wrap.classList.remove('show-progress');
937
+
938
+ // Restore wave
939
+ var wave = $('wave');
940
+ if (wave) wave.style.display = '';
941
+ }
942
+
943
+ // =====================================================================
944
+ // Detail view (product / order cards)
945
+ // =====================================================================
946
+ function ratingStars(r) {
947
+ r = Math.round(r);
948
+ return '\u2605'.repeat(r) + '\u2606'.repeat(5 - r);
949
+ }
950
+
951
+ function showProductDetail(p) {
952
+ var desc = 'Premium ' + p.name.toLowerCase() + ' crafted with breathable mesh upper, responsive cushioning and durable rubber outsole. Perfect for everyday wear and athletic performance.';
953
+ $('detail-question').textContent = 'Would you like to know more about the ' + p.name + '?';
954
+ $('detail-card').innerHTML =
955
+ '<div class="dc-product">' +
956
+ '<img class="pic" src="' + p.img + '" alt="' + escapeHtml(p.name) + '"/>' +
957
+ '<div class="info">' +
958
+ '<div class="pname">' + escapeHtml(p.name) + '</div>' +
959
+ '<div class="rating"><span class="stars">' + ratingStars(4.6) + '</span><span>4.6 (2.4k reviews)</span></div>' +
960
+ '<div class="desc">' + desc + '</div>' +
961
+ '<div class="row">' +
962
+ '<div class="price">$' + p.price + '<span class="cur">USD</span></div>' +
963
+ '<button class="add" title="Add to cart"><i data-lucide="plus" class="w-4 h-4"></i></button>' +
964
+ '</div>' +
965
+ '</div>' +
966
+ '</div>';
967
+ openDetailView();
968
+
969
+ // Send product context to AI with full details (system-level injection — no user bubble)
970
+ var contextMsg = '[system] AUTHORITATIVE DATA from our catalog about a product the customer is currently viewing. ' +
971
+ 'Product name: "' + p.name + '", product ID: ' + p.id + ', price: $' + p.price + ' USD, tag: "' + p.tag + '". ' + desc + ' ' +
972
+ 'The customer wants to know more about this product. Answer directly, treating the data above as the single source of truth. ' +
973
+ 'Do NOT say you cannot find the product, do NOT ask the customer to repeat the product ID, and do NOT transfer to a human agent. ' +
974
+ 'Include key features, price and a brief purchase recommendation. Reply in plain spoken text only, with no markdown or special symbols.';
975
+ sendInterrupt();
976
+ setTimeout(function() {
977
+ sendCustomToAgent({
978
+ type: 20000,
979
+ sender: state.userId,
980
+ receiver: [state.agentUserId],
981
+ payload: { id: uuid(), message: contextMsg, timestamp: Date.now() },
982
+ });
983
+ }, 120);
984
+ }
985
+
986
+ function showOrderDetail(o) {
987
+ var p = window.AppData.products[o.pidx];
988
+ var total = (p.price * o.qty).toFixed(2);
989
+ $('detail-question').textContent = 'Here are the details for order #' + o.id + '. Need any help with it?';
990
+ $('detail-card').innerHTML =
991
+ '<div class="dc-order">' +
992
+ '<div class="ohead">' +
993
+ '<div>' +
994
+ '<div class="oid">Order #' + o.id + '</div>' +
995
+ '<div class="odate">Placed on ' + o.date + '</div>' +
996
+ '</div>' +
997
+ '<span class="badge ' + o.cls + '">' + o.status + '</span>' +
998
+ '</div>' +
999
+ '<div class="obody">' +
1000
+ '<img src="' + p.img + '" alt="' + escapeHtml(p.name) + '"/>' +
1001
+ '<div class="flex-1 min-w-0">' +
1002
+ '<div class="pn">' + escapeHtml(p.name) + '</div>' +
1003
+ '<div class="pq">$' + p.price + ' \u00d7 ' + o.qty + '</div>' +
1004
+ '</div>' +
1005
+ '</div>' +
1006
+ '<div class="ototal">' +
1007
+ '<span class="lbl">Order total</span>' +
1008
+ '<span class="val">$' + total + ' <span style="font-size:11px;color:var(--muted);font-weight:600">USD</span></span>' +
1009
+ '</div>' +
1010
+ '</div>';
1011
+ openDetailView();
1012
+
1013
+ // Send order context to AI with full details (system-level injection — no user bubble)
1014
+ var contextMsg = '[system] AUTHORITATIVE DATA from our order system about an order the customer is currently viewing. ' +
1015
+ 'Order number: ' + o.id + ', placed on ' + o.date + '. ' +
1016
+ 'Product: ' + p.name + ' (ID: ' + p.id + '), quantity ' + o.qty + ' at $' + p.price + ' each, order total $' + total + ' USD. ' +
1017
+ 'Current status: ' + o.status + '. ' +
1018
+ 'Answer the customer directly, treating the data above as the single source of truth. ' +
1019
+ 'Do NOT say the order cannot be found, do NOT ask the customer to repeat the order number, and do NOT transfer to a human agent. ' +
1020
+ 'When you mention the order number, read it digit by digit (for example "one one two two zero three three"), never as a whole number. ' +
1021
+ 'Provide the order status, shipping context and a relevant next step. Reply in plain spoken text only, with no markdown or special symbols.';
1022
+ sendInterrupt();
1023
+ setTimeout(function() {
1024
+ sendCustomToAgent({
1025
+ type: 20000,
1026
+ sender: state.userId,
1027
+ receiver: [state.agentUserId],
1028
+ payload: { id: uuid(), message: contextMsg, timestamp: Date.now() },
1029
+ });
1030
+ }, 120);
1031
+ }
1032
+
1033
+ function openDetailView() {
1034
+ var rc = $('right-console');
1035
+ if (rc) rc.dataset.view = 'detail';
1036
+ $('im-drawer').classList.remove('open');
1037
+ $('dock-detail').style.display = 'flex';
1038
+ $('dock').style.display = 'none';
1039
+ if (window.lucide) lucide.createIcons();
1040
+ }
1041
+
1042
+ function closeDetailView() {
1043
+ var rc = $('right-console');
1044
+ if (rc) rc.dataset.view = 'default';
1045
+ $('dock-detail').style.display = 'none';
1046
+ $('dock').style.display = 'flex';
1047
+ }
1048
+
1049
+ function closeDetailViewSilent() {
1050
+ var rc = $('right-console');
1051
+ if (rc && rc.dataset.view === 'detail') {
1052
+ rc.dataset.view = 'default';
1053
+ $('dock-detail').style.display = 'none';
1054
+ $('dock').style.display = 'flex';
1055
+ }
1056
+ }
1057
+
1058
+ // =====================================================================
1059
+ // Toast
1060
+ // =====================================================================
1061
+ var toastTimer = null;
1062
+ function showToast(msg) {
1063
+ var t = $('kb-toast');
1064
+ if (!t) return;
1065
+ t.textContent = msg;
1066
+ t.classList.add('show');
1067
+ clearTimeout(toastTimer);
1068
+ toastTimer = setTimeout(function() { t.classList.remove('show'); }, 2600);
1069
+ }
1070
+
1071
+ // =====================================================================
1072
+ // Post-call rating card (issue 6)
1073
+ // =====================================================================
1074
+ function showRatingCard(sessionId) {
1075
+ var overlay = $('rating-overlay');
1076
+ if (!overlay) return;
1077
+ state.ratingValue = 0;
1078
+ var submit = $('rating-submit');
1079
+ if (submit) submit.disabled = true;
1080
+ var comment = $('rating-comment');
1081
+ if (comment) comment.value = '';
1082
+ Array.prototype.forEach.call(document.querySelectorAll('.star-btn'), function(b) {
1083
+ b.classList.remove('lit');
1084
+ });
1085
+ overlay.hidden = false;
1086
+ if (window.lucide) lucide.createIcons();
1087
+ }
1088
+
1089
+ function hideRatingCard() {
1090
+ var overlay = $('rating-overlay');
1091
+ if (overlay) overlay.hidden = true;
1092
+ state.lastSessionId = null;
1093
+ }
1094
+
1095
+ function submitRating() {
1096
+ var sid = state.lastSessionId;
1097
+ var rating = state.ratingValue;
1098
+ var commentEl = $('rating-comment');
1099
+ var comment = commentEl ? commentEl.value : '';
1100
+ if (!sid || !rating) return;
1101
+ api('POST', '/api/v1/handoff/feedback', { session_id: sid, rating: rating, comment: comment })
1102
+ .then(function() { addBubble('system', 'Thank you for your feedback!'); })
1103
+ .catch(function(e) { console.warn('[feedback] error', e); });
1104
+ hideRatingCard();
1105
+ }
1106
+
1107
+ // =====================================================================
1108
+ // Render product & order lists
1109
+ // =====================================================================
1110
+ function renderProducts(filter) {
1111
+ var box = $('tab-products');
1112
+ if (!box) return;
1113
+ box.innerHTML = '';
1114
+ var q = (filter || '').trim().toLowerCase();
1115
+ window.AppData.products
1116
+ .filter(function(p) { return !q || p.name.toLowerCase().indexOf(q) !== -1; })
1117
+ .forEach(function(p) {
1118
+ var el = document.createElement('div');
1119
+ el.className = 'product-card';
1120
+ el.innerHTML =
1121
+ '<img class="product-thumb-img" src="' + p.img + '" alt="' + escapeHtml(p.name) + '" loading="lazy"/>' +
1122
+ '<div class="flex-1 min-w-0">' +
1123
+ '<div class="name truncate">' + escapeHtml(p.name) + '</div>' +
1124
+ '<div class="meta">' +
1125
+ '<span class="price">$' + p.price + '</span>' +
1126
+ '<span class="tag ' + p.tagCls + '">' + p.tag + '</span>' +
1127
+ '</div>' +
1128
+ '</div>' +
1129
+ '<i data-lucide="chevron-right" class="w-4 h-4 text-muted flex-shrink-0"></i>';
1130
+ el.addEventListener('click', function() {
1131
+ if (!state.active) {
1132
+ showToast('Please press Start to connect AI before viewing product details.');
1133
+ return;
1134
+ }
1135
+ showProductDetail(p);
1136
+ });
1137
+ box.appendChild(el);
1138
+ });
1139
+ if (window.lucide) lucide.createIcons();
1140
+ }
1141
+
1142
+ function renderOrders(filter) {
1143
+ var box = $('tab-orders');
1144
+ if (!box) return;
1145
+ box.innerHTML = '';
1146
+ var q = (filter || '').trim().toLowerCase();
1147
+ window.AppData.orders
1148
+ .filter(function(o) {
1149
+ var p = window.AppData.products[o.pidx];
1150
+ return !q || o.id.indexOf(q) !== -1 || (p && p.name.toLowerCase().indexOf(q) !== -1) || o.status.toLowerCase().indexOf(q) !== -1;
1151
+ })
1152
+ .forEach(function(o) {
1153
+ var p = window.AppData.products[o.pidx];
1154
+ var el = document.createElement('div');
1155
+ el.className = 'order-card';
1156
+ el.innerHTML =
1157
+ '<div class="top">' +
1158
+ '<div>' +
1159
+ '<div class="oid">Order #' + o.id + '</div>' +
1160
+ '<div class="odate">' + o.date + '</div>' +
1161
+ '</div>' +
1162
+ '<span class="badge ' + o.cls + '">' + o.status + '</span>' +
1163
+ '</div>' +
1164
+ '<div class="body">' +
1165
+ '<img src="' + p.img + '" alt="' + escapeHtml(p.name) + '"/>' +
1166
+ '<div class="min-w-0 flex-1">' +
1167
+ '<div class="pname truncate">' + escapeHtml(p.name) + '</div>' +
1168
+ '<div class="pprice">$' + p.price + ' \u00d7 ' + o.qty + '</div>' +
1169
+ '</div>' +
1170
+ '</div>';
1171
+ el.addEventListener('click', function() {
1172
+ if (!state.active) {
1173
+ showToast('Please press Start to connect AI before viewing order details.');
1174
+ return;
1175
+ }
1176
+ showOrderDetail(o);
1177
+ });
1178
+ box.appendChild(el);
1179
+ });
1180
+ }
1181
+
1182
+ // =====================================================================
1183
+ // Health check
1184
+ // =====================================================================
1185
+ function checkHealth() {
1186
+ api('GET', '/api/v1/health')
1187
+ .then(function(h) {
1188
+ var allOk = h.status === 'ok';
1189
+ state.healthy = allOk;
1190
+ if (!h.configured) {
1191
+ addBubble('system', 'Credentials missing: ' + (h.missing || []).join(', '));
1192
+ }
1193
+ console.log('Backend healthy:', allOk, h.checks);
1194
+ })
1195
+ .catch(function(err) {
1196
+ console.error('[health] error', err);
1197
+ state.healthy = false;
1198
+ });
1199
+ }
1200
+
1201
+ // =====================================================================
1202
+ // INIT
1203
+ // =====================================================================
1204
+ function init() {
1205
+ // Render data
1206
+ if (!window.AppData) {
1207
+ console.warn('AppData not loaded, using fallback');
1208
+ window.AppData = { products: [], orders: [], greeting: 'Hello! How can I help you?' };
1209
+ }
1210
+ renderProducts();
1211
+ renderOrders();
1212
+
1213
+ // Health check
1214
+ checkHealth();
1215
+
1216
+ // Dock buttons
1217
+ $('btn-start').addEventListener('click', function() { start(); });
1218
+ $('btn-mic').addEventListener('click', function() { toggleMute(); });
1219
+ $('btn-agent').addEventListener('click', function() { talkToAgent(); });
1220
+ $('btn-hang').addEventListener('click', function() { requestHangup(); });
1221
+
1222
+ // Detail view dock buttons
1223
+ $('btn-mic-detail').addEventListener('click', function() { toggleMute(); });
1224
+ $('btn-agent-detail').addEventListener('click', function() { talkToAgent(); });
1225
+ $('btn-hang-detail').addEventListener('click', function() {
1226
+ closeDetailViewSilent();
1227
+ requestHangup();
1228
+ });
1229
+
1230
+ // Compact back button
1231
+ $('compact-back').addEventListener('click', function() { closeDetailView(); });
1232
+
1233
+ // Chat controls
1234
+ $('ctl-chat').addEventListener('click', function() { openDrawer(); });
1235
+ $('im-close').addEventListener('click', function() {
1236
+ $('im-drawer').classList.remove('open');
1237
+ });
1238
+
1239
+ // Chat input
1240
+ var input = $('chat-input');
1241
+ var sendBtn = $('send-btn');
1242
+ var submit = function() {
1243
+ var v = input.value;
1244
+ input.value = '';
1245
+ sendText(v);
1246
+ };
1247
+ sendBtn.addEventListener('click', submit);
1248
+ input.addEventListener('keydown', function(e) {
1249
+ if (e.key === 'Enter') submit();
1250
+ });
1251
+
1252
+ // Rating card (issue 6)
1253
+ Array.prototype.forEach.call(document.querySelectorAll('.star-btn'), function(btn) {
1254
+ btn.addEventListener('click', function() {
1255
+ var val = parseInt(btn.getAttribute('data-val'), 10) || 0;
1256
+ state.ratingValue = val;
1257
+ Array.prototype.forEach.call(document.querySelectorAll('.star-btn'), function(b) {
1258
+ b.classList.toggle('lit', parseInt(b.getAttribute('data-val'), 10) <= val);
1259
+ });
1260
+ var submitBtn = $('rating-submit');
1261
+ if (submitBtn) submitBtn.disabled = false;
1262
+ });
1263
+ });
1264
+ $('rating-submit').addEventListener('click', function() { submitRating(); });
1265
+ $('rating-skip').addEventListener('click', function() { hideRatingCard(); });
1266
+
1267
+ // KB Tabs
1268
+ document.querySelectorAll('.kb-tab').forEach(function(t) {
1269
+ t.addEventListener('click', function() {
1270
+ document.querySelectorAll('.kb-tab').forEach(function(x) { x.classList.remove('active'); });
1271
+ t.classList.add('active');
1272
+ var tab = t.dataset.tab;
1273
+ $('tab-products').classList.toggle('hidden', tab !== 'products');
1274
+ $('tab-orders').classList.toggle('hidden', tab !== 'orders');
1275
+ });
1276
+ });
1277
+
1278
+ // Search
1279
+ $('kb-search').addEventListener('input', function(e) {
1280
+ var v = e.target.value;
1281
+ var tab = document.querySelector('.kb-tab.active').dataset.tab;
1282
+ if (tab === 'products') renderProducts(v);
1283
+ else renderOrders(v);
1284
+ });
1285
+
1286
+ // Initial state
1287
+ setMode('pre');
1288
+ if (window.lucide) lucide.createIcons();
1289
+ }
1290
+
1291
+ // Run on DOM ready
1292
+ if (document.readyState === 'loading') {
1293
+ document.addEventListener('DOMContentLoaded', init);
1294
+ } else {
1295
+ init();
1296
+ }
1297
+
1298
+ // Expose for external use
1299
+ window.VoiceAI = {
1300
+ start: start,
1301
+ hangup: hangup,
1302
+ toggleMute: toggleMute,
1303
+ talkToAgent: talkToAgent,
1304
+ sendText: sendText,
1305
+ isActive: function() { return state.active; },
1306
+ };
1307
+ })();