@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,777 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
3
+ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
4
+ import { useUIKit } from '@tencentcloud/uikit-base-component-vue3';
5
+ import {
6
+ ChevronLeft,
7
+ Clock,
8
+ Settings,
9
+ User,
10
+ Phone,
11
+ Mic,
12
+ MicOff,
13
+ Camera,
14
+ CameraOff,
15
+ PhoneOff,
16
+ } from '@/shared/icons';
17
+ import {
18
+ FillMode,
19
+ RoomEvent,
20
+ RoomParticipantEvent,
21
+ RoomParticipantView,
22
+ useDeviceState,
23
+ useRoomParticipantState,
24
+ useRoomState,
25
+ VideoStreamType,
26
+ } from 'tuikit-atomicx-vue3';
27
+ import MedicalBusinessPanel from '@/components/MedicalBusinessPanel.vue';
28
+ import ConsultationManagePanel from '@/components/ConsultationManagePanel.vue';
29
+ import ConsultationVideoStage from '@/features/consultation/components/ConsultationVideoStage.vue';
30
+ import { useConsultationParticipants } from '@/features/consultation/useConsultationParticipants';
31
+ import { useConsultationPermissions } from '@/features/consultation/useConsultationPermissions';
32
+ import { useConsultationDevices } from '@/features/consultation/useConsultationDevices';
33
+ import { formatElapsedDuration } from '@/features/consultation/utils';
34
+ import type {
35
+ ConsultationCallEvent,
36
+ ConsultationParticipantEvent,
37
+ } from '@/features/consultation/types';
38
+ import { services } from '@/services/adapters';
39
+ import { getSessionUser } from '@/utils/session';
40
+ import LoadingSpinner from '@/components/LoadingSpinner.vue';
41
+ import MedicalAlert from '@/components/MedicalAlert.vue';
42
+ import MedicalConfirmDialog from '@/components/MedicalConfirmDialog.vue';
43
+ import LanguageSwitch from '@/components/LanguageSwitch.vue';
44
+
45
+ type CallStage =
46
+ | 'idle'
47
+ | 'calling'
48
+ | 'accepted'
49
+ | 'rejected'
50
+ | 'timeout'
51
+ | 'cancelled';
52
+ const route = useRoute();
53
+ const router = useRouter();
54
+ const roomState = useRoomState();
55
+ const participantState = useRoomParticipantState();
56
+ const deviceState = useDeviceState();
57
+ const sessionUser = getSessionUser();
58
+ const { t } = useUIKit();
59
+
60
+ const appointment = computed(() =>
61
+ services.appointment.getAppointmentById(String(route.params.appointmentId))
62
+ );
63
+ const doctor = computed(() =>
64
+ appointment.value
65
+ ? services.user.getDoctorById(appointment.value.doctorId)
66
+ : null
67
+ );
68
+ const patient = computed(() =>
69
+ appointment.value
70
+ ? services.user.getPatientById(appointment.value.patientId)
71
+ : null
72
+ );
73
+ const callStage = ref<CallStage>('idle');
74
+ const callHint = ref(t('Medical.Consultation.CallIdleHint'));
75
+ const isCalling = ref(false);
76
+ const isCancellingCall = ref(false);
77
+ const deviceAction = ref<'camera' | 'microphone' | ''>('');
78
+ const {
79
+ devicePermissionHint,
80
+ getDeviceErrorHint,
81
+ updateDevicePermissionHint,
82
+ } = useConsultationDevices();
83
+ const finishConfirmVisible = ref(false);
84
+ const consultationClosing = ref(false);
85
+ const finishTargetPath = ref('/doctor/dashboard');
86
+ const elapsedSeconds = ref(0);
87
+
88
+ const patientJoined = computed(() =>
89
+ participantState.participantList.value.some(
90
+ item => item.userId === patient.value?.userId
91
+ )
92
+ );
93
+ const localParticipantInfo = computed(
94
+ () => participantState.localParticipant.value
95
+ );
96
+ const localVideoReady = computed(() =>
97
+ participantState.participantListWithVideo.value.some(
98
+ item => item.userId === localParticipantInfo.value?.userId
99
+ )
100
+ );
101
+
102
+ const isPatientConnected = computed(() => patientJoined.value);
103
+ const currentUserId = computed(() => sessionUser?.userId);
104
+ const primaryDoctorId = computed(() => appointment.value?.doctorId);
105
+ const patientId = computed(() => appointment.value?.patientId);
106
+ const { isPrimaryDoctor, permissions: consultationPermissions } =
107
+ useConsultationPermissions({
108
+ currentUserId,
109
+ primaryDoctorId,
110
+ });
111
+ const isCallingStage = computed(() => callStage.value === 'calling');
112
+ const isErrorStage = computed(() =>
113
+ ['rejected', 'timeout', 'cancelled'].includes(callStage.value)
114
+ );
115
+ const {
116
+ mainMember: mainVideoMember,
117
+ thumbnailMembers: thumbnailVideoMembers,
118
+ focusParticipant,
119
+ } = useConsultationParticipants({
120
+ participantList: participantState.participantList,
121
+ participantListWithVideo: participantState.participantListWithVideo,
122
+ localParticipant: localParticipantInfo,
123
+ primaryDoctorId,
124
+ patientId,
125
+ preferredFocusUserId: patientId,
126
+ getDoctorById: services.user.getDoctorById.bind(services.user),
127
+ getPatientById: services.user.getPatientById.bind(services.user),
128
+ });
129
+ const mainDisplayName = computed(
130
+ () => mainVideoMember.value?.displayName || patient.value?.userName || t('Medical.Common.Patient')
131
+ );
132
+ let durationTimer: number | undefined;
133
+
134
+ function setCallState(stage: CallStage, hint: string) {
135
+ callStage.value = stage;
136
+ callHint.value = hint;
137
+ }
138
+
139
+ async function ensureInRoom() {
140
+ if (!appointment.value) {
141
+ return;
142
+ }
143
+ if (
144
+ !roomState.currentRoom.value ||
145
+ roomState.currentRoom.value.roomId !== appointment.value.roomId
146
+ ) {
147
+ await roomState.joinRoom({ roomId: appointment.value.roomId });
148
+ }
149
+ }
150
+
151
+ async function callPatient() {
152
+ if (
153
+ !appointment.value ||
154
+ !patient.value ||
155
+ isCalling.value ||
156
+ isCancellingCall.value
157
+ ) {
158
+ return;
159
+ }
160
+ isCalling.value = true;
161
+ isCancellingCall.value = false;
162
+ setCallState('calling', t('Medical.Consultation.ConnectingPatient', { patient: patient.value.userName }));
163
+ try {
164
+ await ensureInRoom();
165
+ await roomState.callUserToRoom({
166
+ roomId: appointment.value.roomId,
167
+ userIdList: [patient.value.userId],
168
+ timeout: 30,
169
+ extensionInfo: t('Medical.Consultation.InvitePatientMessage', {
170
+ doctor: doctor.value?.userName ?? t('Medical.Common.Doctor'),
171
+ }),
172
+ });
173
+ } catch (error) {
174
+ setCallState(
175
+ 'rejected',
176
+ error instanceof Error ? error.message : t('Medical.Consultation.CallPatientFailed')
177
+ );
178
+ } finally {
179
+ isCalling.value = false;
180
+ }
181
+ }
182
+
183
+ async function cancelPatientCall() {
184
+ if (
185
+ !appointment.value ||
186
+ !patient.value ||
187
+ !isCallingStage.value ||
188
+ isCancellingCall.value
189
+ ) {
190
+ return;
191
+ }
192
+ isCancellingCall.value = true;
193
+ try {
194
+ await ensureInRoom();
195
+ await roomState.cancelCall({
196
+ roomId: appointment.value.roomId,
197
+ userIdList: [patient.value.userId],
198
+ });
199
+ setCallState('cancelled', t('Medical.Consultation.CallCancelled'));
200
+ } catch (error) {
201
+ setCallState(
202
+ 'rejected',
203
+ error instanceof Error ? error.message : t('Medical.Consultation.CancelCallFailed')
204
+ );
205
+ } finally {
206
+ isCancellingCall.value = false;
207
+ }
208
+ }
209
+
210
+ async function toggleMic() {
211
+ if (deviceAction.value || consultationClosing.value) {
212
+ return;
213
+ }
214
+ deviceAction.value = 'microphone';
215
+ try {
216
+ if (deviceState.microphoneStatus.value === 1) {
217
+ await deviceState.closeLocalMicrophone();
218
+ } else {
219
+ await deviceState.openLocalMicrophone();
220
+ }
221
+ } catch (error) {
222
+ devicePermissionHint.value = getDeviceErrorHint(t('Medical.Device.Microphone'), error);
223
+ } finally {
224
+ deviceAction.value = '';
225
+ }
226
+ }
227
+
228
+ async function toggleCamera() {
229
+ if (deviceAction.value || consultationClosing.value) {
230
+ return;
231
+ }
232
+ deviceAction.value = 'camera';
233
+ try {
234
+ if (deviceState.cameraStatus.value === 1) {
235
+ await deviceState.closeLocalCamera();
236
+ } else {
237
+ await deviceState.openLocalCamera();
238
+ }
239
+ } catch (error) {
240
+ devicePermissionHint.value = getDeviceErrorHint(t('Medical.Device.Camera'), error);
241
+ } finally {
242
+ deviceAction.value = '';
243
+ }
244
+ }
245
+
246
+ async function finishConsultation(targetPath = '/doctor/dashboard') {
247
+ if (consultationClosing.value) {
248
+ return;
249
+ }
250
+ consultationClosing.value = true;
251
+ try {
252
+ await roomState.endRoom();
253
+ await router.replace(targetPath);
254
+ } finally {
255
+ consultationClosing.value = false;
256
+ }
257
+ }
258
+
259
+ function requestFinishConsultation(targetPath = '/doctor/dashboard') {
260
+ if (consultationClosing.value) {
261
+ return;
262
+ }
263
+ finishTargetPath.value = targetPath;
264
+ finishConfirmVisible.value = true;
265
+ }
266
+
267
+ function cancelFinishConsultation() {
268
+ if (consultationClosing.value) {
269
+ return;
270
+ }
271
+ finishConfirmVisible.value = false;
272
+ }
273
+
274
+ async function confirmFinishConsultation() {
275
+ await finishConsultation(finishTargetPath.value);
276
+ finishConfirmVisible.value = false;
277
+ }
278
+
279
+ async function leaveConsultationRoom(targetPath = '/doctor/dashboard') {
280
+ if (consultationClosing.value) {
281
+ return;
282
+ }
283
+ consultationClosing.value = true;
284
+ try {
285
+ await roomState.leaveRoom().catch(() => undefined);
286
+ await router.replace(targetPath);
287
+ } finally {
288
+ consultationClosing.value = false;
289
+ }
290
+ }
291
+
292
+ function handleCallAccepted(eventInfo: ConsultationCallEvent) {
293
+ if (eventInfo.call?.callee?.userId === patient.value?.userId) {
294
+ isCancellingCall.value = false;
295
+ setCallState('accepted', t('Medical.Consultation.PatientAccepted', { patient: patient.value?.userName }));
296
+ }
297
+ }
298
+
299
+ function handleCallRejected(eventInfo: ConsultationCallEvent) {
300
+ if (eventInfo.call?.callee?.userId === patient.value?.userId) {
301
+ isCancellingCall.value = false;
302
+ setCallState('rejected', t('Medical.Consultation.PatientRejected', { patient: patient.value?.userName }));
303
+ }
304
+ }
305
+
306
+ function handleCallTimeout(eventInfo: ConsultationCallEvent) {
307
+ if (eventInfo.call?.callee?.userId === patient.value?.userId) {
308
+ isCancellingCall.value = false;
309
+ setCallState('timeout', t('Medical.Consultation.PatientTimeout', { patient: patient.value?.userName }));
310
+ }
311
+ }
312
+
313
+ function handleCallCancelled(eventInfo: ConsultationCallEvent) {
314
+ if (eventInfo.call?.callee?.userId === patient.value?.userId) {
315
+ isCancellingCall.value = false;
316
+ setCallState('cancelled', t('Medical.Consultation.CallCancelled'));
317
+ }
318
+ }
319
+
320
+ function handleParticipantJoined(eventInfo: ConsultationParticipantEvent) {
321
+ if (eventInfo?.userInfo?.userId === patient.value?.userId) {
322
+ isCancellingCall.value = false;
323
+ setCallState('accepted', t('Medical.Consultation.PatientJoined', { patient: patient.value?.userName }));
324
+ }
325
+ }
326
+
327
+ onMounted(async () => {
328
+ const currentUser = getSessionUser();
329
+ if (!currentUser || currentUser.role !== 'doctor' || !appointment.value) {
330
+ router.replace('/login');
331
+ return;
332
+ }
333
+
334
+ await ensureInRoom();
335
+ // Synchronize current participant state once after joining room.
336
+ await participantState
337
+ .getParticipantList({ cursor: '' })
338
+ .catch(() => undefined);
339
+ const deviceResults = await Promise.allSettled([
340
+ deviceState.openLocalMicrophone(),
341
+ deviceState.openLocalCamera(),
342
+ ]);
343
+ updateDevicePermissionHint(deviceResults);
344
+
345
+ roomState.subscribeEvent(RoomEvent.onCallAccepted, handleCallAccepted);
346
+ roomState.subscribeEvent(RoomEvent.onCallRejected, handleCallRejected);
347
+ roomState.subscribeEvent(RoomEvent.onCallTimeout, handleCallTimeout);
348
+ roomState.subscribeEvent(RoomEvent.onCallCancelled, handleCallCancelled);
349
+ participantState.subscribeEvent(
350
+ RoomParticipantEvent.onParticipantJoined,
351
+ handleParticipantJoined
352
+ );
353
+
354
+ durationTimer = window.setInterval(() => {
355
+ if (isPatientConnected.value) {
356
+ elapsedSeconds.value += 1;
357
+ }
358
+ }, 1000);
359
+ });
360
+
361
+ onBeforeUnmount(() => {
362
+ roomState.unsubscribeEvent(RoomEvent.onCallAccepted, handleCallAccepted);
363
+ roomState.unsubscribeEvent(RoomEvent.onCallRejected, handleCallRejected);
364
+ roomState.unsubscribeEvent(RoomEvent.onCallTimeout, handleCallTimeout);
365
+ roomState.unsubscribeEvent(RoomEvent.onCallCancelled, handleCallCancelled);
366
+ participantState.unsubscribeEvent(
367
+ RoomParticipantEvent.onParticipantJoined,
368
+ handleParticipantJoined
369
+ );
370
+ if (durationTimer) {
371
+ window.clearInterval(durationTimer);
372
+ }
373
+ });
374
+
375
+ watch(isPatientConnected, connected => {
376
+ if (connected && callStage.value !== 'accepted') {
377
+ setCallState('accepted', t('Medical.Consultation.PatientJoined', { patient: patient.value?.userName ?? t('Medical.Common.Patient') }));
378
+ }
379
+ });
380
+
381
+ onBeforeRouteLeave(to => {
382
+ if (consultationClosing.value || !appointment.value) {
383
+ return true;
384
+ }
385
+
386
+ if (isPrimaryDoctor.value) {
387
+ requestFinishConsultation(to.fullPath);
388
+ return false;
389
+ }
390
+
391
+ void leaveConsultationRoom(to.fullPath);
392
+ return false;
393
+ });
394
+ </script>
395
+
396
+ <template>
397
+ <div
398
+ v-if="appointment && patient && doctor"
399
+ class="h-screen bg-[#F8FAFB] flex flex-col overflow-hidden"
400
+ >
401
+ <header
402
+ class="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between shrink-0"
403
+ >
404
+ <div class="flex items-center gap-4">
405
+ <button
406
+ @click="leaveConsultationRoom()"
407
+ :disabled="consultationClosing"
408
+ class="p-2 hover:bg-gray-100 rounded-xl transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
409
+ >
410
+ <LoadingSpinner v-if="consultationClosing" />
411
+ <ChevronLeft v-else :size="20" />
412
+ </button>
413
+ <div class="h-6 w-px bg-gray-200"></div>
414
+ <div>
415
+ <h1 class="font-semibold text-gray-900">{{ t('Medical.Consultation.Title') }}</h1>
416
+ <p class="text-xs text-gray-500">{{ t('Medical.Consultation.AppointmentId', { id: appointment.id }) }}</p>
417
+ </div>
418
+ </div>
419
+
420
+ <div class="flex items-center gap-4">
421
+ <div
422
+ v-if="isPatientConnected"
423
+ class="flex items-center gap-2 bg-[#F1F5F9] px-4 py-2 rounded-xl"
424
+ >
425
+ <Clock :size="16" class="text-[#0D9488]" />
426
+ <span class="font-mono text-sm text-gray-900">{{
427
+ formatElapsedDuration(elapsedSeconds)
428
+ }}</span>
429
+ </div>
430
+ <span
431
+ :class="[
432
+ 'px-3 py-1 rounded-full text-sm font-medium',
433
+ isPatientConnected
434
+ ? 'bg-[#10B981] text-white'
435
+ : 'bg-gray-200 text-gray-700',
436
+ ]"
437
+ >
438
+ {{ isPatientConnected ? t('Medical.Consultation.InCall') : t('Medical.Consultation.WaitingPatient') }}
439
+ </span>
440
+ <div class="h-6 w-px bg-gray-200"></div>
441
+ <LanguageSwitch />
442
+ <button class="p-2 hover:bg-gray-100 rounded-xl transition-colors">
443
+ <Settings :size="20" class="text-gray-600" />
444
+ </button>
445
+ </div>
446
+ </header>
447
+
448
+ <div class="flex-1 min-h-0 flex gap-6 p-6 overflow-hidden">
449
+ <div class="flex-1 flex flex-col gap-6 min-w-0">
450
+ <div
451
+ class="flex-1 border-none shadow-[0_4px_20px_rgba(0,0,0,0.08)] rounded-3xl overflow-hidden bg-gradient-to-br from-gray-900 to-gray-800 relative"
452
+ >
453
+ <div
454
+ :class="[
455
+ 'absolute left-6 z-20 inline-flex items-center gap-2 bg-black/30 rounded-full px-3 py-1.5 backdrop-blur-md',
456
+ isPatientConnected ? 'top-24' : 'top-6',
457
+ ]"
458
+ >
459
+ <span class="flex items-end gap-0.5">
460
+ <i class="inline-block w-1.5 h-3 rounded-full bg-[#00D08A]"></i>
461
+ <i class="inline-block w-1.5 h-4 rounded-full bg-[#00D08A]"></i>
462
+ <i class="inline-block w-1.5 h-5 rounded-full bg-[#00D08A]"></i>
463
+ </span>
464
+ <span class="text-white text-xs leading-none">{{ t('Medical.Consultation.NetworkGood') }}</span>
465
+ </div>
466
+
467
+ <div
468
+ v-if="devicePermissionHint"
469
+ class="absolute left-6 right-6 top-16 z-30"
470
+ >
471
+ <MedicalAlert variant="warning">
472
+ {{ devicePermissionHint }}
473
+ </MedicalAlert>
474
+ </div>
475
+
476
+ <template v-if="!isPatientConnected">
477
+ <div class="absolute inset-0 bg-[#0F1F38]">
478
+ <RoomParticipantView
479
+ v-if="localParticipantInfo"
480
+ class="absolute inset-0"
481
+ :participant="localParticipantInfo"
482
+ :stream-type="VideoStreamType.Camera"
483
+ :fill-mode="FillMode.Fit"
484
+ />
485
+ </div>
486
+
487
+ <div
488
+ class="absolute inset-0 z-10 bg-gradient-to-b from-black/30 via-black/25 to-black/50"
489
+ ></div>
490
+
491
+ <div
492
+ class="absolute top-6 right-6 z-20 rounded-2xl bg-black/35 backdrop-blur-md px-4 py-2.5"
493
+ >
494
+ <p class="text-sm font-semibold text-white leading-none">
495
+ {{ patient.userName }}
496
+ </p>
497
+ <p class="mt-1.5 text-xs leading-none text-white/80">
498
+ {{ t('Medical.Consultation.PatientAge', { gender: appointment.patientGender, age: appointment.patientAge }) }}
499
+ </p>
500
+ </div>
501
+
502
+ <div class="absolute inset-0 z-20 flex items-center justify-center">
503
+ <div class="text-center pointer-events-none">
504
+ <div
505
+ class="w-32 h-32 rounded-full bg-gradient-to-br from-[#0D9488] to-[#0F766E] mx-auto flex items-center justify-center mb-5 shadow-2xl relative"
506
+ >
507
+ <User :size="62" class="text-white" />
508
+ <div
509
+ v-if="isCallingStage"
510
+ class="absolute inset-0 rounded-full border-4 border-[#10B981] animate-ping opacity-70"
511
+ ></div>
512
+ </div>
513
+ <h3 class="text-[36px] leading-none font-semibold text-white">
514
+ {{ patient.userName }}
515
+ </h3>
516
+ <p class="mt-3 text-[20px] leading-none text-white/70">
517
+ {{ isCallingStage ? t('Medical.Consultation.CallingPatient') : t('Medical.Consultation.PendingPatient') }}
518
+ </p>
519
+ <p
520
+ v-if="!localVideoReady"
521
+ class="mt-3 text-xs text-amber-200/90"
522
+ >
523
+ {{ t('Medical.Consultation.CameraPreviewOff') }}
524
+ </p>
525
+ </div>
526
+ </div>
527
+
528
+ <div
529
+ class="absolute left-8 bottom-8 z-20 flex items-center gap-3 rounded-2xl bg-black/35 backdrop-blur-md px-3 py-3"
530
+ >
531
+ <button
532
+ @click="toggleCamera"
533
+ :disabled="!!deviceAction || consultationClosing"
534
+ :class="[
535
+ 'w-12 h-12 rounded-xl transition-colors disabled:opacity-60 disabled:cursor-not-allowed',
536
+ deviceState.cameraStatus.value === 1
537
+ ? 'bg-[#334155]/90 hover:bg-[#475569]'
538
+ : 'bg-red-500/80 hover:bg-red-500',
539
+ ]"
540
+ >
541
+ <LoadingSpinner
542
+ v-if="deviceAction === 'camera'"
543
+ class="mx-auto text-white"
544
+ />
545
+ <component
546
+ v-else
547
+ :is="
548
+ deviceState.cameraStatus.value === 1 ? Camera : CameraOff
549
+ "
550
+ :size="20"
551
+ class="mx-auto text-white"
552
+ />
553
+ </button>
554
+ <button
555
+ @click="toggleMic"
556
+ :disabled="!!deviceAction || consultationClosing"
557
+ :class="[
558
+ 'w-12 h-12 rounded-xl transition-colors disabled:opacity-60 disabled:cursor-not-allowed',
559
+ deviceState.microphoneStatus.value === 1
560
+ ? 'bg-[#334155]/90 hover:bg-[#475569]'
561
+ : 'bg-red-500/80 hover:bg-red-500',
562
+ ]"
563
+ >
564
+ <LoadingSpinner
565
+ v-if="deviceAction === 'microphone'"
566
+ class="mx-auto text-white"
567
+ />
568
+ <component
569
+ v-else
570
+ :is="deviceState.microphoneStatus.value === 1 ? Mic : MicOff"
571
+ :size="20"
572
+ class="mx-auto text-white"
573
+ />
574
+ </button>
575
+ </div>
576
+
577
+ <div
578
+ v-if="isPrimaryDoctor"
579
+ class="absolute left-1/2 bottom-8 -translate-x-1/2 z-20 text-center"
580
+ >
581
+ <div
582
+ class="inline-flex items-center gap-3 rounded-2xl bg-black/35 backdrop-blur-md px-4 py-3"
583
+ >
584
+ <button
585
+ @click="callPatient"
586
+ :disabled="isCalling || isCallingStage || isCancellingCall"
587
+ class="bg-gradient-to-r from-[#10B981] to-[#0D9488] hover:from-[#0D9488] hover:to-[#0F766E] text-white h-12 px-7 rounded-xl text-lg font-semibold gap-2 shadow-xl transition-all disabled:opacity-60 disabled:cursor-not-allowed inline-flex items-center"
588
+ >
589
+ <Phone :size="20" />
590
+ {{ isCallingStage ? t('Medical.Consultation.Calling') : t('Medical.Consultation.CallPatient') }}
591
+ </button>
592
+ <button
593
+ v-if="isCallingStage"
594
+ @click="cancelPatientCall"
595
+ :disabled="isCancellingCall"
596
+ class="h-12 px-5 rounded-xl border border-red-300 text-red-200 hover:bg-red-500/10 transition-colors text-base font-medium disabled:opacity-60 disabled:cursor-not-allowed inline-flex items-center justify-center gap-2"
597
+ >
598
+ <LoadingSpinner v-if="isCancellingCall" />
599
+ {{ isCancellingCall ? t('Medical.Consultation.Canceling') : t('Medical.Consultation.CancelCall') }}
600
+ </button>
601
+ </div>
602
+ <p class="mt-3 text-sm text-white/65">
603
+ {{
604
+ isCallingStage
605
+ ? t('Medical.Consultation.PatientResponding')
606
+ : t('Medical.Consultation.CallIdleHint')
607
+ }}
608
+ </p>
609
+ <p v-if="isErrorStage" class="text-sm text-red-300 mt-2">
610
+ {{ callHint }}
611
+ </p>
612
+ </div>
613
+ <p
614
+ v-else
615
+ class="absolute left-1/2 bottom-10 -translate-x-1/2 z-20 rounded-2xl bg-black/35 px-4 py-3 text-sm text-white/75 backdrop-blur-md"
616
+ >
617
+ {{ t('Medical.Consultation.WaitPrimaryDoctor') }}
618
+ </p>
619
+ </template>
620
+
621
+ <template v-else>
622
+ <ConsultationVideoStage
623
+ :main-member="mainVideoMember"
624
+ :thumbnail-members="thumbnailVideoMembers"
625
+ thumbnail-list-class="top-6 right-6 max-h-[calc(100%-150px)] w-56"
626
+ thumbnail-card-class="h-36"
627
+ @focus="focusParticipant"
628
+ />
629
+
630
+ <div
631
+ class="absolute top-6 left-6 bg-black/40 backdrop-blur-md text-white px-6 py-3 rounded-2xl"
632
+ >
633
+ <div class="flex items-center gap-3">
634
+ <div
635
+ class="w-10 h-10 rounded-full bg-[#0D9488] flex items-center justify-center text-white font-medium"
636
+ >
637
+ {{ mainVideoMember?.avatarText || t('Medical.Consultation.PatientAvatarFallback') }}
638
+ </div>
639
+ <div>
640
+ <p class="font-medium">{{ mainDisplayName }}</p>
641
+ <p class="text-xs text-white/70">
642
+ {{ mainVideoMember?.roleLabel }}
643
+ </p>
644
+ </div>
645
+ </div>
646
+ </div>
647
+
648
+ <div
649
+ class="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-3 bg-black/40 backdrop-blur-md px-6 py-4 rounded-2xl"
650
+ >
651
+ <button
652
+ @click="toggleMic"
653
+ :disabled="!!deviceAction || consultationClosing"
654
+ :class="[
655
+ 'w-14 h-14 rounded-full transition-colors disabled:opacity-60 disabled:cursor-not-allowed',
656
+ deviceState.microphoneStatus.value === 1
657
+ ? 'bg-gray-700 hover:bg-gray-600'
658
+ : 'bg-red-500 hover:bg-red-600',
659
+ ]"
660
+ >
661
+ <LoadingSpinner
662
+ v-if="deviceAction === 'microphone'"
663
+ class="mx-auto text-white"
664
+ />
665
+ <component
666
+ v-else
667
+ :is="deviceState.microphoneStatus.value === 1 ? Mic : MicOff"
668
+ :size="24"
669
+ class="mx-auto text-white"
670
+ />
671
+ </button>
672
+ <button
673
+ @click="toggleCamera"
674
+ :disabled="!!deviceAction || consultationClosing"
675
+ :class="[
676
+ 'w-14 h-14 rounded-full transition-colors disabled:opacity-60 disabled:cursor-not-allowed',
677
+ deviceState.cameraStatus.value === 1
678
+ ? 'bg-gray-700 hover:bg-gray-600'
679
+ : 'bg-red-500 hover:bg-red-600',
680
+ ]"
681
+ >
682
+ <LoadingSpinner
683
+ v-if="deviceAction === 'camera'"
684
+ class="mx-auto text-white"
685
+ />
686
+ <component
687
+ v-else
688
+ :is="
689
+ deviceState.cameraStatus.value === 1 ? Camera : CameraOff
690
+ "
691
+ :size="24"
692
+ class="mx-auto text-white"
693
+ />
694
+ </button>
695
+ <button
696
+ @click="
697
+ isPrimaryDoctor
698
+ ? requestFinishConsultation()
699
+ : leaveConsultationRoom()
700
+ "
701
+ :disabled="consultationClosing"
702
+ class="w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
703
+ >
704
+ <LoadingSpinner
705
+ v-if="consultationClosing"
706
+ class="mx-auto text-white"
707
+ />
708
+ <PhoneOff v-else :size="24" class="mx-auto text-white" />
709
+ </button>
710
+ </div>
711
+ </template>
712
+ </div>
713
+
714
+ <div
715
+ class="p-6 bg-white border-none shadow-[0_2px_8px_rgba(0,0,0,0.04)] rounded-2xl"
716
+ >
717
+ <div class="grid grid-cols-4 gap-6">
718
+ <div>
719
+ <p class="text-xs text-gray-500 mb-1">{{ t('Medical.Consultation.ChiefComplaint') }}</p>
720
+ <p class="text-sm text-gray-900 font-medium">
721
+ {{ appointment.chiefComplaint }}
722
+ </p>
723
+ </div>
724
+ <div>
725
+ <p class="text-xs text-gray-500 mb-1">{{ t('Medical.Consultation.AllergyHistory') }}</p>
726
+ <p class="text-sm text-red-600 font-medium">
727
+ {{ appointment.allergyHistory }}
728
+ </p>
729
+ </div>
730
+ <div>
731
+ <p class="text-xs text-gray-500 mb-1">{{ t('Medical.Consultation.MedicalHistory') }}</p>
732
+ <p class="text-sm text-gray-900">
733
+ {{ appointment.medicalHistory }}
734
+ </p>
735
+ </div>
736
+ <div>
737
+ <p class="text-xs text-gray-500 mb-1">{{ t('Medical.Consultation.Phone') }}</p>
738
+ <p class="text-sm text-gray-900 font-mono">
739
+ {{ appointment.patientPhone }}
740
+ </p>
741
+ </div>
742
+ </div>
743
+ </div>
744
+ </div>
745
+
746
+ <div
747
+ :class="[
748
+ 'h-full min-h-0 shrink-0 flex gap-4',
749
+ isPrimaryDoctor ? 'w-[860px]' : 'w-[360px]',
750
+ ]"
751
+ >
752
+ <div v-if="isPrimaryDoctor" class="flex-1 min-w-0">
753
+ <MedicalBusinessPanel :appointment="appointment" :patient="patient" />
754
+ </div>
755
+ <div class="w-[320px] min-w-0">
756
+ <ConsultationManagePanel
757
+ :doctor="doctor"
758
+ :patient="patient"
759
+ :permissions="consultationPermissions"
760
+ />
761
+ </div>
762
+ </div>
763
+ </div>
764
+
765
+ <MedicalConfirmDialog
766
+ :visible="finishConfirmVisible"
767
+ :title="t('Medical.Consultation.FinishTitle')"
768
+ :message="t('Medical.Consultation.FinishMessage')"
769
+ :cancel-text="t('Medical.Consultation.ContinueConsultation')"
770
+ :confirm-text="t('Medical.Consultation.ConfirmFinish')"
771
+ :loading="consultationClosing"
772
+ danger
773
+ @cancel="cancelFinishConsultation"
774
+ @confirm="confirmFinishConsultation"
775
+ />
776
+ </div>
777
+ </template>