@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.
- package/.cursor/rules/main.mdc +12 -0
- package/AGENTS.md +14 -104
- package/CLAUDE.md +15 -127
- package/CODEBUDDY.md +32 -118
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/bin/cli.js +133 -41
- package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
- package/hooks/cursor-adapter.py +45 -18
- package/hooks/hooks-cursor.json +5 -18
- package/hooks/hooks.json +6 -31
- package/knowledge-base/conference/web/index.yaml +143 -0
- package/knowledge-base/platform-slice-template.md +1041 -133
- package/knowledge-base/products.yaml +76 -0
- package/knowledge-base/scenario-spec.md +1316 -115
- package/knowledge-base/scenarios/conference/base/general-conference.md +22 -2
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +14 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +19 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +13 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +12 -0
- package/knowledge-base/slice-spec.md +2377 -299
- package/knowledge-base/slices/conference/web/integration-audit.md +1 -1
- package/knowledge-base/slices/conference/web/login-auth.md +14 -1
- package/knowledge-base/tooling/aliases.yaml +92 -0
- package/knowledge-base/tooling/intent-signals.yaml +181 -0
- package/knowledge-base/tooling/symptom-keywords.yaml +21 -0
- package/package.json +1 -1
- package/skills/trtc/SKILL.md +202 -245
- package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
- package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_read.py +12 -8
- package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_write.py +16 -12
- package/skills/{trtc-topic/guardrails → trtc/hooks}/stop_require_apply_evidence.py +22 -14
- package/skills/trtc/hooks/topic_phase_gate.py +161 -0
- package/skills/{trtc-topic → trtc}/runtime/README.md +2 -2
- package/skills/{trtc-onboarding/reference/reporting-protocol.md → trtc/runtime/REPORTING.md} +5 -5
- package/skills/{trtc-topic → trtc}/runtime/RUNTIME.md +5 -5
- package/skills/trtc/runtime/lib/__init__.py +0 -0
- package/skills/{trtc-topic → trtc}/runtime/package-lock.json +2 -2
- package/skills/{trtc-topic → trtc}/runtime/package.json +2 -2
- package/skills/{trtc-topic → trtc}/runtime/telemetry-bridge.mjs +1 -1
- package/skills/{trtc-topic/scripts → trtc/tools}/STATE-MACHINE-GUIDE.md +23 -23
- package/skills/trtc/tools/__init__.py +2 -0
- package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
- package/skills/trtc/tools/apply.py +540 -0
- package/skills/trtc/tools/docs.py +712 -0
- package/skills/trtc/tools/docsbot.py +182 -0
- package/skills/trtc/tools/entry/render_ai_instructions.py +92 -0
- package/skills/trtc/tools/flow.py +1089 -0
- package/skills/{trtc-topic/scripts → trtc/tools}/init_slice_queue.py +5 -4
- package/skills/{trtc-topic/scripts → trtc/tools}/next_slice.py +5 -4
- package/skills/trtc/tools/query_classifier.py +301 -0
- package/skills/trtc/tools/reporting.py +447 -0
- package/skills/trtc/tools/search.py +817 -0
- package/skills/trtc/tools/session.py +1261 -0
- package/skills/trtc/tools/state_machine.py +690 -0
- package/skills/trtc-ai-service/README.md +195 -0
- package/skills/trtc-ai-service/README.zh-CN.md +193 -0
- package/skills/trtc-ai-service/SKILL.md +945 -0
- package/skills/trtc-ai-service/auto_adapters/README.md +40 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/README.md +27 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/angular/voice-agent.component.ts.tpl +131 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/manifest.yaml +57 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/react/VoiceAgent.tsx.tpl +142 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/vue/VoiceAgent.vue.tpl +121 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-backend.md +45 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-frontend.md +51 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-rest-api.md +93 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/README.md +25 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/manifest.yaml +30 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/quarkus/VoiceAgentFilter.java.tpl +64 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/springboot/VoiceAgentFilter.java.tpl +91 -0
- package/skills/trtc-ai-service/auto_adapters/manifest.yaml +43 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/README.md +25 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/express.js.tpl +40 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/fastify.js.tpl +27 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/koa.js.tpl +31 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/manifest.yaml +47 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/README.md +22 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/django.py.tpl +32 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/fastapi.py.tpl +35 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/flask.py.tpl +31 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/manifest.yaml +45 -0
- package/skills/trtc-ai-service/capabilities/__init__.py +43 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/.env.example +29 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/INTEGRATION.md +134 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/INTERFACE_ADAPT.md +111 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/QUICK_START.md +62 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/manifest.yaml +250 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/requirements.txt +6 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/__init__.py +10 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/_capability_loader.py +218 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/agent.py +231 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +132 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/health.py +355 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/log_filter.py +76 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/modality.py +109 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/server.py +312 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/trtc_client.py +315 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/usersig.py +90 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/tests/test_skeleton.py +216 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/README.md +48 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/app.js +415 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/index.html +68 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/styles.css +136 -0
- package/skills/trtc-ai-service/capabilities/digital-human/README.md +31 -0
- package/skills/trtc-ai-service/capabilities/digital-human/manifest.yaml +64 -0
- package/skills/trtc-ai-service/capabilities/digital-human/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/digital-human/src/router.py +43 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/INTERFACE_ADAPT.md +353 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/README.md +44 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/manifest.yaml +227 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/__init__.py +9 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/default_rest.py +242 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/factory.py +89 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/local_queue.py +258 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/mock.py +132 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/__init__.py +25 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/intent_detector.py +75 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/models.py +163 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/service.py +192 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/feedback_store.py +54 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/__init__.py +4 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/handoff_client.py +86 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/queue.py +62 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/router.py +201 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/summary_link.py +77 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/trigger.py +25 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/INTERFACE_ADAPT.md +297 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/README.md +51 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/data/faq.json +20 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/manifest.yaml +211 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/__init__.py +8 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/__init__.py +9 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/default_rest.py +209 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/factory.py +86 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/local_json.py +172 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/mock.py +91 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/__init__.py +12 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/models.py +77 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/scoring.py +73 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/service.py +78 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/__init__.py +4 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/kb_client.py +61 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/retriever.py +56 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/router.py +85 -0
- package/skills/trtc-ai-service/capabilities/session-summary/INTERFACE_ADAPT.md +99 -0
- package/skills/trtc-ai-service/capabilities/session-summary/README.md +47 -0
- package/skills/trtc-ai-service/capabilities/session-summary/data/test_session.json +18 -0
- package/skills/trtc-ai-service/capabilities/session-summary/manifest.yaml +165 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/__init__.py +5 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/base.py +31 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/default_rest.py +67 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/factory.py +51 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/local_json.py +42 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/mock.py +22 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/recorder.py +210 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/router.py +93 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/summarizer.py +163 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/INTERFACE_ADAPT.md +158 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/README.md +50 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/data/tools.yaml +58 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/examples/__init__.py +1 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/examples/local_tools.py +101 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/manifest.yaml +146 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/__init__.py +8 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/dispatcher.py +54 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/registry.py +219 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/router.py +50 -0
- package/skills/trtc-ai-service/references/business-contract-spec.md +263 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/README.md +86 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/output-templates/recipe.yaml.j2 +194 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q1-business-scenario.md +43 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q2-io-modality.md +57 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q3-ui-form.md +55 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q4-capabilities.md +78 -0
- package/skills/trtc-ai-service/scenarios/customer-service/README.md +114 -0
- package/skills/trtc-ai-service/scenarios/customer-service/recipe.yaml +154 -0
- package/skills/trtc-ai-service/scenarios/customer-service/sample-data/README.md +32 -0
- package/skills/trtc-ai-service/scenarios/customer-service/sample-data/faq-sample.json +37 -0
- package/skills/trtc-ai-service/scenarios/customer-service/system-prompt.template.md +94 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/app.js +347 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/index.html +125 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/styles.css +487 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/tokens.css +71 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/design-system/DESIGN_GUIDELINES.md +370 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/README.md +68 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/app.js +1307 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/data.js +40 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/index.html +233 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/mock-shop.json +21 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/styles.css +603 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/tokens.css +71 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/agent-link.js +323 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/app.js +458 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/index.html +109 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/styles.css +489 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/tokens.css +59 -0
- package/skills/trtc-ai-service/scripts/add-capability.py +364 -0
- package/skills/trtc-ai-service/scripts/contract-adapt.py +334 -0
- package/skills/trtc-ai-service/scripts/detect-stack.py +40 -0
- package/skills/trtc-ai-service/scripts/lib/__init__.py +20 -0
- package/skills/trtc-ai-service/scripts/lib/adapter_codegen.py +509 -0
- package/skills/trtc-ai-service/scripts/lib/arbitrator.py +152 -0
- package/skills/trtc-ai-service/scripts/lib/contract_resolver.py +519 -0
- package/skills/trtc-ai-service/scripts/lib/credential_validators.py +253 -0
- package/skills/trtc-ai-service/scripts/lib/curl_parser.py +303 -0
- package/skills/trtc-ai-service/scripts/lib/degrader.py +140 -0
- package/skills/trtc-ai-service/scripts/lib/injector.py +347 -0
- package/skills/trtc-ai-service/scripts/lib/manifest_resolver.py +288 -0
- package/skills/trtc-ai-service/scripts/lib/openapi_parser.py +289 -0
- package/skills/trtc-ai-service/scripts/lib/stack_detector.py +159 -0
- package/skills/trtc-ai-service/scripts/lib/tokens_compile.py +204 -0
- package/skills/trtc-ai-service/scripts/post-install-patch.py +225 -0
- package/skills/trtc-ai-service/scripts/setup-credentials.py +393 -0
- package/skills/trtc-ai-service/scripts/verify-credentials.py +108 -0
- package/skills/trtc-ai-service/start.sh +111 -0
- package/skills/trtc-ai-service/tests/__init__.py +1 -0
- package/skills/trtc-ai-service/tests/test_arbitrator.py +64 -0
- package/skills/trtc-ai-service/tests/test_capability_overlay.py +85 -0
- package/skills/trtc-ai-service/tests/test_contract_resolver.py +190 -0
- package/skills/trtc-ai-service/tests/test_handoff_ports.py +195 -0
- package/skills/trtc-ai-service/tests/test_kb_ports.py +195 -0
- package/skills/trtc-ai-service/tests/test_manifest_resolver.py +95 -0
- package/skills/trtc-ai-service/tests/test_recipe_assembly.py +175 -0
- package/skills/trtc-ai-service/tests/test_stack_and_degrader.py +101 -0
- package/skills/trtc-ai-service/tests/test_verify_credentials.py +285 -0
- package/skills/trtc-ai-service/triggers.yaml +29 -0
- package/skills/trtc-conference/SKILL.md +324 -0
- package/skills/trtc-conference/flows/onboarding.md +205 -0
- package/skills/trtc-conference/flows/topic.md +474 -0
- package/skills/trtc-conference/flows/troubleshoot.md +85 -0
- package/skills/trtc-conference/hooks/pretooluse_require_business_decisions.py +213 -0
- package/skills/trtc-conference/playbooks/medical-quickstart.md +84 -0
- package/skills/trtc-conference/playbooks/official-roomkit.md +97 -0
- package/skills/trtc-conference/references/local-usersig/basic-info-config.ts +39 -0
- package/skills/trtc-conference/references/usersig-handling.md +134 -0
- package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/test_conference_index_contract.py +43 -0
- package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +103 -0
- package/skills/trtc-conference/tests/test_conference_template_contract.py +25 -0
- package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +132 -0
- package/skills/trtc-conference/tools/apply_checks.py +328 -0
- package/skills/trtc-conference/verify_lib/__init__.py +0 -0
- package/skills/{trtc-apply/guardrails/apply_lib → trtc-conference/verify_lib}/rule_parser.py +1 -1
- package/skills/trtc-docs/SKILL.md +91 -119
- package/.cursor/rules/ui-mode.mdc +0 -99
- package/ai-instructions/base.md +0 -13
- package/ai-instructions/ui-mode.md +0 -93
- package/knowledge-base/index.yaml +0 -462
- package/skills/trtc/room-builder/SKILL.md +0 -133
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +0 -108
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +0 -226
- package/skills/trtc-apply/SKILL.md +0 -97
- package/skills/trtc-onboarding/SKILL.md +0 -841
- package/skills/trtc-onboarding/reference/path-a1-demo.md +0 -103
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +0 -737
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +0 -186
- package/skills/trtc-onboarding/reference/path-c-expand.md +0 -43
- package/skills/trtc-onboarding/reference/supported-matrix.md +0 -100
- package/skills/trtc-search/SKILL.md +0 -228
- package/skills/trtc-topic/SKILL.md +0 -622
- package/skills/trtc-topic/scripts/apply.py +0 -581
- package/skills/trtc-topic/scripts/lib/state_machine.py +0 -328
- package/skills/trtc-topic/tests/README.md +0 -70
- package/skills/trtc-topic/tests/conftest.py +0 -72
- package/skills/trtc-topic/tests/test_apply_cli.py +0 -480
- package/skills/trtc-topic/tests/test_end_to_end.py +0 -305
- package/skills/trtc-topic/tests/test_finalize_session.py +0 -51
- package/skills/trtc-topic/tests/test_gates.py +0 -316
- package/skills/trtc-topic/tests/test_session_resolver.py +0 -260
- package/skills/trtc-topic/tests/test_state_machine.py +0 -414
- package/skills/trtc-topic/tests/test_stop_require_apply.py +0 -99
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +0 -130
- /package/skills/{trtc-topic → trtc}/runtime/lib/platforms.py +0 -0
- /package/skills/{trtc-topic → trtc}/runtime/telemetry_collector.py +0 -0
- /package/skills/{trtc-onboarding/reference → trtc/runtime}/usersig-handling.md +0 -0
- /package/skills/{trtc-topic/scripts → trtc/tools}/finalize_session.py +0 -0
- /package/skills/{trtc-apply/guardrails/apply_lib → trtc-ai-service/capabilities/conversation-core/tests}/__init__.py +0 -0
- /package/skills/{trtc-topic → trtc-conference}/references/execution-units.yaml +0 -0
- /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
- /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
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/backend-contract.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/integration.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/theme.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/index.html +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/package.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/postcss.config.js +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/App.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/ConsultationManagePanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LanguageSwitch.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LoadingSpinner.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalAlert.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalBusinessPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalButton.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalConfirmDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalDataPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalRecordPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/PrescriptionPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/basic-info-config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/runtime-config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/env.d.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/types.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationChat.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationDevices.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationParticipants.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationPermissions.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/utils.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/en-US/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/medicalTranslate.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/state.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/zh-CN/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/main.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/appointments.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/users.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/router/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/appointmentService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/authService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/launchContext.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/userService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/appointmentService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/authService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/userService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/types.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/shared/icons.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/index.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/tailwind.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/theme.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/auth.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/format.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/navigation.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/session.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorConsultationView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorDashboardView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/LoginView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationFinishedView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientSelectDoctorView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientWaitingView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.node.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/vite.config.ts +0 -0
- /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
- /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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
})();
|