@tencent-rtc/trtc-agent-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/README.zh.md +173 -0
  4. package/bin/cli.js +434 -0
  5. package/knowledge-base/index.yaml +454 -0
  6. package/knowledge-base/platform-slice-template.md +233 -0
  7. package/knowledge-base/scenario-spec.md +350 -0
  8. package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
  9. package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
  10. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
  11. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
  12. package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
  13. package/knowledge-base/slice-spec.md +546 -0
  14. package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
  15. package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
  16. package/knowledge-base/slices/conference/web/device-control.md +338 -0
  17. package/knowledge-base/slices/conference/web/login-auth.md +261 -0
  18. package/knowledge-base/slices/conference/web/network-quality.md +190 -0
  19. package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
  20. package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
  21. package/knowledge-base/slices/conference/web/participant-list.md +238 -0
  22. package/knowledge-base/slices/conference/web/participant-management.md +718 -0
  23. package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
  24. package/knowledge-base/slices/conference/web/room-call.md +213 -0
  25. package/knowledge-base/slices/conference/web/room-chat.md +426 -0
  26. package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
  27. package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
  28. package/knowledge-base/slices/conference/web/screen-share.md +211 -0
  29. package/knowledge-base/slices/conference/web/video-layout.md +675 -0
  30. package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
  31. package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
  32. package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
  33. package/knowledge-base/slices/live/anchor-preview.md +90 -0
  34. package/knowledge-base/slices/live/anchor-room-config.md +104 -0
  35. package/knowledge-base/slices/live/audience-list.md +86 -0
  36. package/knowledge-base/slices/live/audience-manage.md +92 -0
  37. package/knowledge-base/slices/live/audience-watch.md +85 -0
  38. package/knowledge-base/slices/live/audio.md +116 -0
  39. package/knowledge-base/slices/live/barrage.md +88 -0
  40. package/knowledge-base/slices/live/beauty.md +99 -0
  41. package/knowledge-base/slices/live/coguest-apply.md +105 -0
  42. package/knowledge-base/slices/live/device-control.md +91 -0
  43. package/knowledge-base/slices/live/error-codes.md +167 -0
  44. package/knowledge-base/slices/live/gift.md +84 -0
  45. package/knowledge-base/slices/live/ios/.gitkeep +0 -0
  46. package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
  47. package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
  48. package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
  49. package/knowledge-base/slices/live/ios/audience-list.md +353 -0
  50. package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
  51. package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
  52. package/knowledge-base/slices/live/ios/audio.md +373 -0
  53. package/knowledge-base/slices/live/ios/barrage.md +285 -0
  54. package/knowledge-base/slices/live/ios/beauty.md +323 -0
  55. package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
  56. package/knowledge-base/slices/live/ios/device-control.md +286 -0
  57. package/knowledge-base/slices/live/ios/error-codes.md +270 -0
  58. package/knowledge-base/slices/live/ios/gift.md +315 -0
  59. package/knowledge-base/slices/live/ios/live-list.md +269 -0
  60. package/knowledge-base/slices/live/ios/login-auth.md +247 -0
  61. package/knowledge-base/slices/live/live-list.md +82 -0
  62. package/knowledge-base/slices/live/login-auth.md +78 -0
  63. package/package.json +34 -0
  64. package/skills/trtc/SKILL.md +326 -0
  65. package/skills/trtc/room-builder/SKILL.md +138 -0
  66. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
  67. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
  68. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
  69. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
  70. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
  71. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
  72. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
  73. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
  74. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
  75. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
  76. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
  77. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
  78. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
  79. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
  80. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
  81. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
  82. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
  83. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
  84. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
  85. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  86. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  87. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
  88. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
  89. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
  90. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
  91. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
  92. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
  93. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
  94. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
  95. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
  96. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
  97. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
  98. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
  99. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
  100. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
  101. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
  102. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
  103. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
  104. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
  105. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
  106. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
  107. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
  108. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
  109. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
  110. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
  111. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
  112. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
  113. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
  114. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
  115. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
  116. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
  117. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
  118. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
  119. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
  120. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
  121. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
  122. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
  123. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
  124. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
  125. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
  126. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
  127. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
  128. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
  129. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
  130. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
  131. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
  132. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
  133. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
  134. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
  135. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
  136. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
  137. package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
  138. package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
  139. package/skills/trtc-apply/SKILL.md +97 -0
  140. package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
  141. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  142. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
  143. package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
  144. package/skills/trtc-docs/SKILL.md +207 -0
  145. package/skills/trtc-onboarding/SKILL.md +839 -0
  146. package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
  147. package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
  148. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
  149. package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
  150. package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
  151. package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
  152. package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
  153. package/skills/trtc-search/SKILL.md +221 -0
  154. package/skills/trtc-topic/SKILL.md +638 -0
  155. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
  156. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
  157. package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
  158. package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
  159. package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
  160. package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
  161. package/skills/trtc-topic/references/execution-units.yaml +58 -0
  162. package/skills/trtc-topic/runtime/README.md +50 -0
  163. package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
  164. package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
  165. package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
  166. package/skills/trtc-topic/runtime/package-lock.json +1211 -0
  167. package/skills/trtc-topic/runtime/package.json +13 -0
  168. package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
  169. package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
  170. package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
  171. package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
  172. package/skills/trtc-topic/scripts/apply.py +581 -0
  173. package/skills/trtc-topic/scripts/finalize_session.py +113 -0
  174. package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
  175. package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
  176. package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
  177. package/skills/trtc-topic/scripts/next_slice.py +137 -0
  178. package/skills/trtc-topic/tests/README.md +70 -0
  179. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
  180. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  181. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
  182. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  183. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
  184. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
  185. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
  186. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
  187. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
  188. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
  189. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
  190. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
  191. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
  192. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
  193. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
  194. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
  195. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
  196. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
  197. package/skills/trtc-topic/tests/conftest.py +72 -0
  198. package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
  199. package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
  200. package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
  201. package/skills/trtc-topic/tests/test_gates.py +316 -0
  202. package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
  203. package/skills/trtc-topic/tests/test_state_machine.py +414 -0
  204. package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
  205. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
@@ -0,0 +1,838 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
3
+ import { useUIKit } from '@tencentcloud/uikit-base-component-vue3';
4
+ import { MessageSquare, PenLine, Users } from '@/shared/icons';
5
+ import {
6
+ DeviceStatus,
7
+ DeviceType,
8
+ RoomParticipantEvent,
9
+ useAITranscriberState,
10
+ useRoomParticipantState,
11
+ useRoomState,
12
+ } from 'tuikit-atomicx-vue3';
13
+ import type {
14
+ ConsultationMemberCard,
15
+ ConsultationPermissions,
16
+ ConsultationTranscriptItem,
17
+ DoctorInviteListItem,
18
+ } from '@/features/consultation/types';
19
+ import { services, type MedicalUser } from '@/services/adapters';
20
+ import ConsultationChatPanel from '@/features/consultation/components/ConsultationChatPanel.vue';
21
+ import ConsultationMembersPanel from '@/features/consultation/components/ConsultationMembersPanel.vue';
22
+ import ConsultationTranscriptionPanel from '@/features/consultation/components/ConsultationTranscriptionPanel.vue';
23
+ import InviteDoctorDialog from '@/features/consultation/components/InviteDoctorDialog.vue';
24
+ import KickMemberConfirmDialog from '@/features/consultation/components/KickMemberConfirmDialog.vue';
25
+ import { useConsultationChat } from '@/features/consultation/useConsultationChat';
26
+ import {
27
+ formatConsultationClock,
28
+ normalizeConsultationTimestamp,
29
+ } from '@/features/consultation/utils';
30
+
31
+ const props = defineProps<{
32
+ doctor: MedicalUser;
33
+ patient: MedicalUser;
34
+ permissions?: ConsultationPermissions;
35
+ }>();
36
+ const { t } = useUIKit();
37
+
38
+ type PanelTab = 'chat' | 'transcribe' | 'members';
39
+ type DeviceInviteKey = 'camera' | 'microphone';
40
+ type DeviceInviteStatus =
41
+ | 'idle'
42
+ | 'pending'
43
+ | 'accepted'
44
+ | 'declined'
45
+ | 'timeout'
46
+ | 'cancelled';
47
+
48
+ const activeTab = ref<PanelTab>('chat');
49
+ const permissions = computed<ConsultationPermissions>(() => ({
50
+ canInviteDoctor: props.permissions?.canInviteDoctor ?? true,
51
+ canCancelInvite: props.permissions?.canCancelInvite ?? true,
52
+ canKickMember: props.permissions?.canKickMember ?? true,
53
+ canManagePatientDevice: props.permissions?.canManagePatientDevice ?? true,
54
+ }));
55
+
56
+ const roomState = useRoomState();
57
+ const participantState = useRoomParticipantState();
58
+ const {
59
+ realtimeMessageList,
60
+ startRealtimeTranscriber,
61
+ stopRealtimeTranscriber,
62
+ } = useAITranscriberState();
63
+
64
+ const consultationSessionStartedAt = ref(Date.now());
65
+ const patientUserId = computed(() => props.patient.userId);
66
+ const {
67
+ activeConversation,
68
+ chatInput,
69
+ chatError,
70
+ isSendingMessage,
71
+ chatMessages,
72
+ ensureChatConversation,
73
+ handleSendMessage,
74
+ clearChatConversation,
75
+ } = useConsultationChat({
76
+ peerUserId: patientUserId,
77
+ sessionStartedAt: consultationSessionStartedAt,
78
+ });
79
+
80
+ const transcriberRunning = ref(false);
81
+ const transcriberBusy = ref(false);
82
+ const transcriberHint = ref('');
83
+ const isCopyingDraft = ref(false);
84
+ const isExportingDraft = ref(false);
85
+ let clearingSession = false;
86
+
87
+ const inviteDialogVisible = ref(false);
88
+ const inviteKeyword = ref('');
89
+ const inviteFeedback = ref('');
90
+ const invitingDoctorId = ref<string | null>(null);
91
+ const cancellingDoctorId = ref<string | null>(null);
92
+ const kickingUserId = ref<string | null>(null);
93
+ const kickConfirmTarget = ref<{ userId: string; userName: string } | null>(
94
+ null
95
+ );
96
+ const patientDeviceInvitePending = ref<Record<DeviceInviteKey, boolean>>({
97
+ camera: false,
98
+ microphone: false,
99
+ });
100
+ const patientDeviceInviteLoading = ref<Record<DeviceInviteKey, boolean>>({
101
+ camera: false,
102
+ microphone: false,
103
+ });
104
+ const patientDeviceInviteResult = ref<Record<DeviceInviteKey, DeviceInviteStatus>>({
105
+ camera: 'idle',
106
+ microphone: 'idle',
107
+ });
108
+
109
+ const participantUserIdSet = computed(
110
+ () => new Set(participantState.participantList.value.map(item => item.userId))
111
+ );
112
+ const pendingUserIdSet = computed(
113
+ () =>
114
+ new Set(
115
+ participantState.pendingParticipantList.value.map(item => item.userId)
116
+ )
117
+ );
118
+
119
+ const transcriptList = computed<ConsultationTranscriptItem[]>(() =>
120
+ [...realtimeMessageList.value]
121
+ .filter(item => item.sourceText?.trim())
122
+ .filter(
123
+ item =>
124
+ normalizeConsultationTimestamp(item.timestamp) >=
125
+ consultationSessionStartedAt.value
126
+ )
127
+ .sort(
128
+ (a, b) =>
129
+ normalizeConsultationTimestamp(a.timestamp) -
130
+ normalizeConsultationTimestamp(b.timestamp)
131
+ )
132
+ );
133
+
134
+ const transcriptText = computed(() =>
135
+ transcriptList.value
136
+ .map(
137
+ item =>
138
+ `[${formatConsultationClock(item.timestamp)}] ${getSpeakerName(item.speakerUserId)}:${item.sourceText}`
139
+ )
140
+ .join('\n')
141
+ );
142
+
143
+ const doctorInviteList = computed<DoctorInviteListItem[]>(() => {
144
+ const keyword = inviteKeyword.value.trim().toLowerCase();
145
+ return services.user
146
+ .listDoctors()
147
+ .filter(item => item.userId !== props.doctor.userId)
148
+ .map(item => {
149
+ let inviteStatus: 'idle' | 'pending' | 'joined' = 'idle';
150
+ if (participantUserIdSet.value.has(item.userId)) {
151
+ inviteStatus = 'joined';
152
+ } else if (pendingUserIdSet.value.has(item.userId)) {
153
+ inviteStatus = 'pending';
154
+ }
155
+ return { ...item, inviteStatus };
156
+ })
157
+ .filter(item => item.inviteStatus !== 'joined')
158
+ .filter(item => {
159
+ if (!keyword) {
160
+ return true;
161
+ }
162
+ return [item.userName, item.department, item.title]
163
+ .filter(Boolean)
164
+ .some(field => String(field).toLowerCase().includes(keyword));
165
+ });
166
+ });
167
+
168
+ const memberCards = computed<ConsultationMemberCard[]>(() => [
169
+ ...participantState.participantList.value.map(item => {
170
+ const doctorInfo = services.user.getDoctorById(item.userId);
171
+ const patientInfo = services.user.getPatientById(item.userId);
172
+ const displayName =
173
+ item.nameCard ||
174
+ doctorInfo?.userName ||
175
+ patientInfo?.userName ||
176
+ item.userName ||
177
+ item.userId;
178
+ const isPatient = item.userId === props.patient.userId;
179
+ const isPrimaryDoctor = item.userId === props.doctor.userId;
180
+ return {
181
+ userId: item.userId,
182
+ userName: displayName,
183
+ isPatient,
184
+ roleLabel: getMemberRoleLabel(isPatient, isPrimaryDoctor),
185
+ roleClass: getMemberRoleClass(isPatient, isPrimaryDoctor),
186
+ avatarClass: getMemberAvatarClass(isPatient, isPrimaryDoctor),
187
+ cameraOn: item.cameraStatus === DeviceStatus.On,
188
+ microphoneOn: item.microphoneStatus === DeviceStatus.On,
189
+ pending: false,
190
+ cancelable: false,
191
+ };
192
+ }),
193
+ ...participantState.pendingParticipantList.value
194
+ .filter(item => !participantUserIdSet.value.has(item.userId))
195
+ .map(item => {
196
+ const doctorInfo = services.user.getDoctorById(item.userId);
197
+ return {
198
+ userId: item.userId,
199
+ userName: doctorInfo?.userName || item.userName || item.userId,
200
+ isPatient: false,
201
+ roleLabel: t('Medical.Manage.PendingJoin'),
202
+ roleClass: 'bg-[#F3F4F6] text-[#4B5563]',
203
+ avatarClass: 'from-[#CBD5E1] to-[#94A3B8]',
204
+ cameraOn: false,
205
+ microphoneOn: false,
206
+ pending: true,
207
+ cancelable: !!doctorInfo && item.userId !== props.doctor.userId,
208
+ };
209
+ }),
210
+ ]);
211
+
212
+ const patientCameraOn = computed(() =>
213
+ participantState.participantList.value.some(
214
+ item =>
215
+ item.userId === props.patient.userId &&
216
+ item.cameraStatus === DeviceStatus.On
217
+ )
218
+ );
219
+ const patientMicrophoneOn = computed(() =>
220
+ participantState.participantList.value.some(
221
+ item =>
222
+ item.userId === props.patient.userId &&
223
+ item.microphoneStatus === DeviceStatus.On
224
+ )
225
+ );
226
+
227
+ watch(
228
+ () => props.patient.userId,
229
+ async () => {
230
+ consultationSessionStartedAt.value = Date.now();
231
+ resetPatientDeviceInvite();
232
+ await ensureChatConversation();
233
+ },
234
+ { immediate: true }
235
+ );
236
+
237
+ watch(
238
+ () => inviteDialogVisible.value,
239
+ visible => {
240
+ if (!visible) {
241
+ inviteKeyword.value = '';
242
+ inviteFeedback.value = '';
243
+ invitingDoctorId.value = null;
244
+ cancellingDoctorId.value = null;
245
+ }
246
+ }
247
+ );
248
+
249
+ watch(patientCameraOn, cameraOn => {
250
+ if (cameraOn && patientDeviceInvitePending.value.camera) {
251
+ setDeviceInviteState('camera', false, 'accepted');
252
+ }
253
+ });
254
+
255
+ watch(patientMicrophoneOn, microphoneOn => {
256
+ if (microphoneOn && patientDeviceInvitePending.value.microphone) {
257
+ setDeviceInviteState('microphone', false, 'accepted');
258
+ }
259
+ });
260
+
261
+ function getMemberRoleLabel(isPatient: boolean, isPrimaryDoctor: boolean) {
262
+ if (isPatient) {
263
+ return t('Medical.Common.Patient');
264
+ }
265
+ return isPrimaryDoctor ? t('Medical.Manage.PrimaryDoctor') : t('Medical.Common.ConsultingDoctor');
266
+ }
267
+
268
+ function getMemberRoleClass(isPatient: boolean, isPrimaryDoctor: boolean) {
269
+ if (isPatient) {
270
+ return 'bg-[#E0ECFF] text-[#1D4ED8]';
271
+ }
272
+ return isPrimaryDoctor
273
+ ? 'bg-[#FEF3C7] text-[#92400E]'
274
+ : 'bg-[#DCFCE7] text-[#166534]';
275
+ }
276
+
277
+ function getMemberAvatarClass(isPatient: boolean, isPrimaryDoctor: boolean) {
278
+ if (isPatient) {
279
+ return 'from-[#3B82F6] to-[#2563EB]';
280
+ }
281
+ return isPrimaryDoctor
282
+ ? 'from-[#0D9488] to-[#0F766E]'
283
+ : 'from-[#14B8A6] to-[#0F766E]';
284
+ }
285
+
286
+ async function clearConsultationSessionData() {
287
+ if (clearingSession) {
288
+ return;
289
+ }
290
+ clearingSession = true;
291
+ try {
292
+ inviteFeedback.value = '';
293
+ await clearChatConversation();
294
+ if (transcriberRunning.value) {
295
+ await stopRealtimeTranscriber().catch(() => undefined);
296
+ transcriberRunning.value = false;
297
+ }
298
+ } finally {
299
+ clearingSession = false;
300
+ }
301
+ }
302
+
303
+ function getSpeakerName(userId: string) {
304
+ if (!userId) {
305
+ return t('Medical.Manage.UnknownMember');
306
+ }
307
+ const participant = participantState.participantList.value.find(
308
+ item => item.userId === userId
309
+ );
310
+ if (participant) {
311
+ return participant.nameCard || participant.userName || participant.userId;
312
+ }
313
+ if (userId === props.doctor.userId) {
314
+ return props.doctor.userName;
315
+ }
316
+ if (userId === props.patient.userId) {
317
+ return props.patient.userName;
318
+ }
319
+ return (
320
+ services.user.getDoctorById(userId)?.userName ||
321
+ services.user.getPatientById(userId)?.userName ||
322
+ userId
323
+ );
324
+ }
325
+
326
+ async function toggleRealtimeTranscriber() {
327
+ if (transcriberBusy.value) {
328
+ return;
329
+ }
330
+ transcriberBusy.value = true;
331
+ transcriberHint.value = '';
332
+ try {
333
+ if (transcriberRunning.value) {
334
+ await stopRealtimeTranscriber();
335
+ transcriberRunning.value = false;
336
+ transcriberHint.value = t('Medical.Manage.TranscriberClosed');
337
+ return;
338
+ }
339
+ await startRealtimeTranscriber({
340
+ sourceLanguage: 'zh',
341
+ translationLanguages: ['en'],
342
+ });
343
+ transcriberRunning.value = true;
344
+ transcriberHint.value = t('Medical.Manage.TranscriberStarted');
345
+ } catch (error) {
346
+ transcriberHint.value =
347
+ error instanceof Error ? error.message : t('Medical.Manage.TranscriberFailed');
348
+ } finally {
349
+ transcriberBusy.value = false;
350
+ }
351
+ }
352
+
353
+ async function copyDraft() {
354
+ if (isCopyingDraft.value) {
355
+ return;
356
+ }
357
+ if (!transcriptText.value) {
358
+ transcriberHint.value = t('Medical.Manage.NoCopyContent');
359
+ return;
360
+ }
361
+ isCopyingDraft.value = true;
362
+ try {
363
+ await navigator.clipboard.writeText(transcriptText.value);
364
+ transcriberHint.value = t('Medical.Manage.Copied');
365
+ } catch {
366
+ transcriberHint.value = t('Medical.Manage.CopyFailed');
367
+ } finally {
368
+ isCopyingDraft.value = false;
369
+ }
370
+ }
371
+
372
+ function exportDraft() {
373
+ if (isExportingDraft.value) {
374
+ return;
375
+ }
376
+ if (!transcriptText.value) {
377
+ transcriberHint.value = t('Medical.Manage.NoExportContent');
378
+ return;
379
+ }
380
+ isExportingDraft.value = true;
381
+ try {
382
+ const blob = new Blob([transcriptText.value], {
383
+ type: 'text/plain;charset=utf-8',
384
+ });
385
+ const fileUrl = URL.createObjectURL(blob);
386
+ const link = document.createElement('a');
387
+ link.href = fileUrl;
388
+ link.download = `consultation-draft-${Date.now()}.txt`;
389
+ link.click();
390
+ URL.revokeObjectURL(fileUrl);
391
+ transcriberHint.value = t('Medical.Manage.Exported');
392
+ } finally {
393
+ isExportingDraft.value = false;
394
+ }
395
+ }
396
+
397
+ async function inviteDoctor(userId: string) {
398
+ if (!permissions.value.canInviteDoctor) {
399
+ inviteFeedback.value = t('Medical.Manage.NoInvitePermission');
400
+ return;
401
+ }
402
+ const roomId = roomState.currentRoom.value?.roomId;
403
+ if (!roomId) {
404
+ inviteFeedback.value = t('Medical.Manage.NotInRoomInvite');
405
+ return;
406
+ }
407
+ invitingDoctorId.value = userId;
408
+ inviteFeedback.value = '';
409
+ try {
410
+ await roomState.callUserToRoom({
411
+ roomId: String(roomId),
412
+ userIdList: [userId],
413
+ timeout: 45,
414
+ extensionInfo: t('Medical.Manage.InviteExtension', {
415
+ doctor: props.doctor.userName,
416
+ }),
417
+ });
418
+ inviteFeedback.value = t('Medical.Manage.InviteSent');
419
+ await participantState.getParticipantList({ cursor: '' });
420
+ } catch (error) {
421
+ inviteFeedback.value =
422
+ error instanceof Error ? error.message : t('Medical.Manage.InviteFailed');
423
+ } finally {
424
+ invitingDoctorId.value = null;
425
+ }
426
+ }
427
+
428
+ async function cancelInvite(userId: string) {
429
+ if (!permissions.value.canCancelInvite) {
430
+ inviteFeedback.value = t('Medical.Manage.NoCancelInvitePermission');
431
+ return;
432
+ }
433
+ const roomId = roomState.currentRoom.value?.roomId;
434
+ if (!roomId) {
435
+ inviteFeedback.value = t('Medical.Manage.NotInRoomCancelInvite');
436
+ return;
437
+ }
438
+ cancellingDoctorId.value = userId;
439
+ inviteFeedback.value = '';
440
+ try {
441
+ await roomState.cancelCall({
442
+ roomId: String(roomId),
443
+ userIdList: [userId],
444
+ });
445
+ inviteFeedback.value = t('Medical.Manage.InviteCancelled');
446
+ await participantState.getParticipantList({ cursor: '' });
447
+ } catch (error) {
448
+ inviteFeedback.value =
449
+ error instanceof Error ? error.message : t('Medical.Manage.CancelInviteFailed');
450
+ } finally {
451
+ cancellingDoctorId.value = null;
452
+ }
453
+ }
454
+
455
+ function getDeviceKey(deviceType: DeviceType): DeviceInviteKey | null {
456
+ if (deviceType === DeviceType.Camera) {
457
+ return 'camera';
458
+ }
459
+ if (deviceType === DeviceType.Microphone) {
460
+ return 'microphone';
461
+ }
462
+ return null;
463
+ }
464
+
465
+ function getDeviceLabel(deviceType: DeviceType) {
466
+ return deviceType === DeviceType.Camera ? t('Medical.Device.Camera') : t('Medical.Device.Microphone');
467
+ }
468
+
469
+ function setDeviceInviteLoading(key: DeviceInviteKey, loading: boolean) {
470
+ patientDeviceInviteLoading.value = {
471
+ ...patientDeviceInviteLoading.value,
472
+ [key]: loading,
473
+ };
474
+ }
475
+
476
+ function setDeviceInviteState(
477
+ key: DeviceInviteKey,
478
+ pending: boolean,
479
+ status: DeviceInviteStatus
480
+ ) {
481
+ patientDeviceInvitePending.value = {
482
+ ...patientDeviceInvitePending.value,
483
+ [key]: pending,
484
+ };
485
+ patientDeviceInviteResult.value = {
486
+ ...patientDeviceInviteResult.value,
487
+ [key]: status,
488
+ };
489
+ }
490
+
491
+ function resetPatientDeviceInvite() {
492
+ patientDeviceInvitePending.value = { camera: false, microphone: false };
493
+ patientDeviceInviteLoading.value = { camera: false, microphone: false };
494
+ patientDeviceInviteResult.value = { camera: 'idle', microphone: 'idle' };
495
+ }
496
+
497
+ async function invitePatientOpenDevice(deviceType: DeviceType) {
498
+ if (!permissions.value.canManagePatientDevice || !props.patient?.userId) {
499
+ return;
500
+ }
501
+ const key = getDeviceKey(deviceType);
502
+ if (!key) {
503
+ return;
504
+ }
505
+ setDeviceInviteLoading(key, true);
506
+ inviteFeedback.value = '';
507
+ try {
508
+ await participantState.inviteToOpenDevice({
509
+ userId: props.patient.userId,
510
+ device: deviceType,
511
+ timeout: 30,
512
+ });
513
+ setDeviceInviteState(key, true, 'pending');
514
+ inviteFeedback.value = t('Medical.Manage.InvitedPatientOpenDevice', {
515
+ device: getDeviceLabel(deviceType),
516
+ });
517
+ } catch (error) {
518
+ inviteFeedback.value =
519
+ error instanceof Error
520
+ ? error.message
521
+ : t('Medical.Manage.InviteOpenDeviceFailed', {
522
+ device: getDeviceLabel(deviceType),
523
+ });
524
+ } finally {
525
+ setDeviceInviteLoading(key, false);
526
+ }
527
+ }
528
+
529
+ async function cancelPatientOpenDeviceInvitation(deviceType: DeviceType) {
530
+ if (!permissions.value.canManagePatientDevice || !props.patient?.userId) {
531
+ return;
532
+ }
533
+ const key = getDeviceKey(deviceType);
534
+ if (!key) {
535
+ return;
536
+ }
537
+ setDeviceInviteLoading(key, true);
538
+ inviteFeedback.value = '';
539
+ try {
540
+ await participantState.cancelOpenDeviceInvitation({
541
+ userId: props.patient.userId,
542
+ device: deviceType,
543
+ });
544
+ setDeviceInviteState(key, false, 'cancelled');
545
+ inviteFeedback.value = t('Medical.Manage.CancelledPatientDeviceInvite', {
546
+ device: getDeviceLabel(deviceType),
547
+ });
548
+ } catch (error) {
549
+ inviteFeedback.value =
550
+ error instanceof Error
551
+ ? error.message
552
+ : t('Medical.Manage.CancelDeviceInviteFailed', {
553
+ device: getDeviceLabel(deviceType),
554
+ });
555
+ } finally {
556
+ setDeviceInviteLoading(key, false);
557
+ }
558
+ }
559
+
560
+ async function closePatientDevice(deviceType: DeviceType) {
561
+ if (!permissions.value.canManagePatientDevice || !props.patient?.userId) {
562
+ return;
563
+ }
564
+ const key = getDeviceKey(deviceType);
565
+ if (!key) {
566
+ return;
567
+ }
568
+ setDeviceInviteLoading(key, true);
569
+ inviteFeedback.value = '';
570
+ try {
571
+ await participantState.closeParticipantDevice({
572
+ userId: props.patient.userId,
573
+ deviceType,
574
+ });
575
+ setDeviceInviteState(key, false, 'idle');
576
+ inviteFeedback.value = t('Medical.Manage.ClosedPatientDevice', {
577
+ device: getDeviceLabel(deviceType),
578
+ });
579
+ } catch (error) {
580
+ inviteFeedback.value =
581
+ error instanceof Error
582
+ ? error.message
583
+ : t('Medical.Manage.CloseDeviceFailed', {
584
+ device: getDeviceLabel(deviceType),
585
+ });
586
+ } finally {
587
+ setDeviceInviteLoading(key, false);
588
+ }
589
+ }
590
+
591
+ function requestKickMember(member: { userId: string; userName: string }) {
592
+ if (!permissions.value.canKickMember) {
593
+ inviteFeedback.value = t('Medical.Manage.NoKickPermission');
594
+ return;
595
+ }
596
+ kickConfirmTarget.value = member;
597
+ }
598
+
599
+ function cancelKickConfirm() {
600
+ if (!kickingUserId.value) {
601
+ kickConfirmTarget.value = null;
602
+ }
603
+ }
604
+
605
+ async function confirmKickMember() {
606
+ if (!permissions.value.canKickMember) {
607
+ inviteFeedback.value = t('Medical.Manage.NoKickPermission');
608
+ kickConfirmTarget.value = null;
609
+ return;
610
+ }
611
+ if (!kickConfirmTarget.value) {
612
+ return;
613
+ }
614
+ const { userId } = kickConfirmTarget.value;
615
+ kickingUserId.value = userId;
616
+ inviteFeedback.value = '';
617
+ try {
618
+ await participantState.kickUser({ userId });
619
+ inviteFeedback.value = t('Medical.Manage.MemberKicked');
620
+ kickConfirmTarget.value = null;
621
+ } catch (error) {
622
+ inviteFeedback.value =
623
+ error instanceof Error ? error.message : t('Medical.Manage.KickFailed');
624
+ } finally {
625
+ kickingUserId.value = null;
626
+ }
627
+ }
628
+
629
+ const onPatientDeviceInvitationAccepted = (options: {
630
+ invitation: { deviceType: DeviceType };
631
+ operator: { userId: string };
632
+ }) => {
633
+ if (options.operator.userId !== props.patient.userId) {
634
+ return;
635
+ }
636
+ const key = getDeviceKey(options.invitation.deviceType);
637
+ if (!key) {
638
+ return;
639
+ }
640
+ setDeviceInviteState(key, false, 'accepted');
641
+ inviteFeedback.value = t('Medical.Manage.PatientAcceptedDeviceInvite', {
642
+ device: getDeviceLabel(options.invitation.deviceType),
643
+ });
644
+ };
645
+
646
+ const onPatientDeviceInvitationDeclined = (options: {
647
+ invitation: { deviceType: DeviceType };
648
+ operator: { userId: string };
649
+ }) => {
650
+ if (options.operator.userId !== props.patient.userId) {
651
+ return;
652
+ }
653
+ const key = getDeviceKey(options.invitation.deviceType);
654
+ if (!key) {
655
+ return;
656
+ }
657
+ setDeviceInviteState(key, false, 'declined');
658
+ inviteFeedback.value = t('Medical.Manage.PatientDeclinedDeviceInvite', {
659
+ device: getDeviceLabel(options.invitation.deviceType),
660
+ });
661
+ };
662
+
663
+ const onPatientDeviceInvitationTimeout = (options: {
664
+ invitation: { deviceType: DeviceType };
665
+ }) => {
666
+ const key = getDeviceKey(options.invitation.deviceType);
667
+ if (!key || !patientDeviceInvitePending.value[key]) {
668
+ return;
669
+ }
670
+ setDeviceInviteState(key, false, 'timeout');
671
+ inviteFeedback.value = t('Medical.Manage.DeviceInviteTimeout', {
672
+ device: getDeviceLabel(options.invitation.deviceType),
673
+ });
674
+ };
675
+
676
+ const onPatientDeviceInvitationCancelled = (options: {
677
+ invitation: { deviceType: DeviceType };
678
+ }) => {
679
+ const key = getDeviceKey(options.invitation.deviceType);
680
+ if (!key || !patientDeviceInvitePending.value[key]) {
681
+ return;
682
+ }
683
+ setDeviceInviteState(key, false, 'cancelled');
684
+ inviteFeedback.value = t('Medical.Manage.DeviceInviteCancelled', {
685
+ device: getDeviceLabel(options.invitation.deviceType),
686
+ });
687
+ };
688
+
689
+ onMounted(() => {
690
+ participantState.subscribeEvent(
691
+ RoomParticipantEvent.onDeviceInvitationAccepted,
692
+ onPatientDeviceInvitationAccepted
693
+ );
694
+ participantState.subscribeEvent(
695
+ RoomParticipantEvent.onDeviceInvitationDeclined,
696
+ onPatientDeviceInvitationDeclined
697
+ );
698
+ participantState.subscribeEvent(
699
+ RoomParticipantEvent.onDeviceInvitationTimeout,
700
+ onPatientDeviceInvitationTimeout
701
+ );
702
+ participantState.subscribeEvent(
703
+ RoomParticipantEvent.onDeviceInvitationCancelled,
704
+ onPatientDeviceInvitationCancelled
705
+ );
706
+ });
707
+
708
+ onBeforeUnmount(() => {
709
+ participantState.unsubscribeEvent(
710
+ RoomParticipantEvent.onDeviceInvitationAccepted,
711
+ onPatientDeviceInvitationAccepted
712
+ );
713
+ participantState.unsubscribeEvent(
714
+ RoomParticipantEvent.onDeviceInvitationDeclined,
715
+ onPatientDeviceInvitationDeclined
716
+ );
717
+ participantState.unsubscribeEvent(
718
+ RoomParticipantEvent.onDeviceInvitationTimeout,
719
+ onPatientDeviceInvitationTimeout
720
+ );
721
+ participantState.unsubscribeEvent(
722
+ RoomParticipantEvent.onDeviceInvitationCancelled,
723
+ onPatientDeviceInvitationCancelled
724
+ );
725
+ void clearConsultationSessionData();
726
+ });
727
+ </script>
728
+
729
+ <template>
730
+ <div
731
+ class="h-full min-h-0 bg-white border-none shadow-[0_4px_20px_rgba(0,0,0,0.08)] rounded-3xl overflow-hidden flex flex-col"
732
+ >
733
+ <div class="w-full grid grid-cols-3 p-2 bg-gray-50 border-b border-gray-100">
734
+ <button
735
+ @click="activeTab = 'chat'"
736
+ :class="[
737
+ 'flex items-center justify-center gap-2 px-3 py-2.5 rounded-xl font-medium text-sm transition-all',
738
+ activeTab === 'chat'
739
+ ? 'bg-white text-[#0D9488] shadow-sm'
740
+ : 'text-gray-600 hover:text-gray-900',
741
+ ]"
742
+ >
743
+ <MessageSquare :size="16" />
744
+ {{ t('Medical.Manage.Chat') }}
745
+ </button>
746
+ <button
747
+ @click="activeTab = 'transcribe'"
748
+ :class="[
749
+ 'flex items-center justify-center gap-2 px-3 py-2.5 rounded-xl font-medium text-sm transition-all',
750
+ activeTab === 'transcribe'
751
+ ? 'bg-white text-[#0D9488] shadow-sm'
752
+ : 'text-gray-600 hover:text-gray-900',
753
+ ]"
754
+ >
755
+ <PenLine :size="16" />
756
+ {{ t('Medical.Manage.Transcribe') }}
757
+ </button>
758
+ <button
759
+ @click="activeTab = 'members'"
760
+ :class="[
761
+ 'flex items-center justify-center gap-2 px-3 py-2.5 rounded-xl font-medium text-sm transition-all',
762
+ activeTab === 'members'
763
+ ? 'bg-white text-[#0D9488] shadow-sm'
764
+ : 'text-gray-600 hover:text-gray-900',
765
+ ]"
766
+ >
767
+ <Users :size="16" />
768
+ {{ t('Medical.Manage.Members') }}
769
+ </button>
770
+ </div>
771
+
772
+ <div class="flex-1 min-h-0 bg-white">
773
+ <ConsultationChatPanel
774
+ v-if="activeTab === 'chat'"
775
+ v-model:chat-input="chatInput"
776
+ :doctor="doctor"
777
+ :patient="patient"
778
+ :messages="chatMessages"
779
+ :active-conversation-id="activeConversation?.conversationID"
780
+ :chat-error="chatError"
781
+ :is-sending-message="isSendingMessage"
782
+ @send="handleSendMessage"
783
+ />
784
+ <ConsultationTranscriptionPanel
785
+ v-else-if="activeTab === 'transcribe'"
786
+ :transcript-list="transcriptList"
787
+ :transcript-text="transcriptText"
788
+ :transcriber-running="transcriberRunning"
789
+ :transcriber-busy="transcriberBusy"
790
+ :transcriber-hint="transcriberHint"
791
+ :is-copying-draft="isCopyingDraft"
792
+ :is-exporting-draft="isExportingDraft"
793
+ :get-speaker-name="getSpeakerName"
794
+ @toggle="toggleRealtimeTranscriber"
795
+ @copy="copyDraft"
796
+ @export="exportDraft"
797
+ />
798
+ <ConsultationMembersPanel
799
+ v-else
800
+ :member-cards="memberCards"
801
+ :permissions="permissions"
802
+ :invite-feedback="inviteFeedback"
803
+ :kicking-user-id="kickingUserId"
804
+ :cancelling-doctor-id="cancellingDoctorId"
805
+ :patient-device-invite-pending="patientDeviceInvitePending"
806
+ :patient-device-invite-loading="patientDeviceInviteLoading"
807
+ @open-invite="inviteDialogVisible = true"
808
+ @request-kick="requestKickMember"
809
+ @cancel-invite="cancelInvite"
810
+ @invite-patient-open-device="invitePatientOpenDevice"
811
+ @cancel-patient-open-device-invitation="
812
+ cancelPatientOpenDeviceInvitation
813
+ "
814
+ @close-patient-device="closePatientDevice"
815
+ />
816
+ </div>
817
+ </div>
818
+
819
+ <KickMemberConfirmDialog
820
+ :target="kickConfirmTarget"
821
+ :kicking-user-id="kickingUserId"
822
+ @cancel="cancelKickConfirm"
823
+ @confirm="confirmKickMember"
824
+ />
825
+
826
+ <InviteDoctorDialog
827
+ v-if="permissions.canInviteDoctor"
828
+ v-model:invite-keyword="inviteKeyword"
829
+ :visible="inviteDialogVisible"
830
+ :doctor-invite-list="doctorInviteList"
831
+ :invite-feedback="inviteFeedback"
832
+ :inviting-doctor-id="invitingDoctorId"
833
+ :cancelling-doctor-id="cancellingDoctorId"
834
+ @close="inviteDialogVisible = false"
835
+ @invite="inviteDoctor"
836
+ @cancel-invite="cancelInvite"
837
+ />
838
+ </template>