audex 1.0.7a3__py3-none-any.whl
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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dependency_injector.wiring import Provide
|
|
4
|
+
from dependency_injector.wiring import inject
|
|
5
|
+
from fastapi import Depends
|
|
6
|
+
from nicegui import ui
|
|
7
|
+
|
|
8
|
+
from audex.container import Container
|
|
9
|
+
from audex.service.doctor import DoctorService
|
|
10
|
+
from audex.service.session import SessionService
|
|
11
|
+
from audex.view.decorators import handle_errors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@ui.page("/sessions")
|
|
15
|
+
@handle_errors
|
|
16
|
+
@inject
|
|
17
|
+
async def render(
|
|
18
|
+
doctor_service: DoctorService = Depends(Provide[Container.service.doctor]),
|
|
19
|
+
session_service: SessionService = Depends(Provide[Container.service.session]),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Render sessions history page with clean design."""
|
|
22
|
+
|
|
23
|
+
# Get current doctor
|
|
24
|
+
doctor = await doctor_service.current_doctor()
|
|
25
|
+
|
|
26
|
+
# Add CSS
|
|
27
|
+
ui.add_head_html('<link rel="stylesheet" href="/static/css/sessions/styles.css">')
|
|
28
|
+
|
|
29
|
+
# Fetch sessions
|
|
30
|
+
sessions = await session_service.list(doctor_id=doctor.id, page_size=100)
|
|
31
|
+
|
|
32
|
+
# Dialog Functions
|
|
33
|
+
async def show_delete_dialog(session_id: str, session_name: str):
|
|
34
|
+
"""Show delete confirmation dialog."""
|
|
35
|
+
with (
|
|
36
|
+
ui.dialog() as dialog,
|
|
37
|
+
ui.card().classes("dialog-card").style("width: 450px; padding: 28px;"),
|
|
38
|
+
):
|
|
39
|
+
with ui.row().classes("w-full items-center mb-6"):
|
|
40
|
+
ui.icon("warning", size="xl").classes("text-warning")
|
|
41
|
+
ui.label("确认删除").classes("text-h5 font-bold text-grey-9 ml-3 flex-1")
|
|
42
|
+
ui.button(icon="close", on_click=dialog.close).props("flat round dense")
|
|
43
|
+
|
|
44
|
+
ui.label(f"确定要删除会话「{session_name}」吗?").classes("text-body1 text-grey-8 mb-2")
|
|
45
|
+
ui.label("此操作不可恢复。").classes("text-body2 text-grey-7 mb-6")
|
|
46
|
+
|
|
47
|
+
with ui.row().classes("w-full gap-3 justify-end"):
|
|
48
|
+
ui.button("取消", on_click=dialog.close).props(
|
|
49
|
+
"outline color=grey-8 no-caps"
|
|
50
|
+
).classes("action-btn btn-secondary")
|
|
51
|
+
|
|
52
|
+
async def do_delete():
|
|
53
|
+
dialog.close()
|
|
54
|
+
try:
|
|
55
|
+
await session_service.delete(session_id)
|
|
56
|
+
ui.notify("会话已删除", type="positive", position="top")
|
|
57
|
+
ui.navigate.to("/sessions")
|
|
58
|
+
except Exception:
|
|
59
|
+
ui.notify("删除失败", type="negative", position="top")
|
|
60
|
+
|
|
61
|
+
ui.button("删除", on_click=do_delete).props(
|
|
62
|
+
"unelevated color=negative no-caps"
|
|
63
|
+
).classes("action-btn")
|
|
64
|
+
|
|
65
|
+
dialog.open()
|
|
66
|
+
|
|
67
|
+
# Header
|
|
68
|
+
with ui.header().classes("header-glass items-center justify-between px-6 py-4"):
|
|
69
|
+
with ui.row().classes("items-center gap-3"):
|
|
70
|
+
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to("/")).props("flat round")
|
|
71
|
+
ui.label("历史会话").classes("text-h6 font-semibold text-grey-9")
|
|
72
|
+
|
|
73
|
+
with ui.row().classes("items-center gap-2"):
|
|
74
|
+
ui.button(
|
|
75
|
+
"导出", icon="download", on_click=lambda: ui.navigate.to("/sessions/export")
|
|
76
|
+
).props("flat no-caps").classes("header-btn export-btn")
|
|
77
|
+
|
|
78
|
+
ui.button("新建录音", on_click=lambda: ui.navigate.to("/recording")).props(
|
|
79
|
+
"unelevated color=primary no-caps"
|
|
80
|
+
).classes("header-btn")
|
|
81
|
+
|
|
82
|
+
# Main Content
|
|
83
|
+
if not sessions:
|
|
84
|
+
with (
|
|
85
|
+
ui.element("div")
|
|
86
|
+
.classes("w-full bg-white")
|
|
87
|
+
.style(
|
|
88
|
+
"display: flex; align-items: center; justify-content: center; "
|
|
89
|
+
"min-height: calc(100vh - 64px);"
|
|
90
|
+
),
|
|
91
|
+
ui.element("div").classes("empty-state"),
|
|
92
|
+
):
|
|
93
|
+
ui.icon("chat_bubble_outline", size="4em").classes("text-grey-4 mb-4")
|
|
94
|
+
ui.label("还没有会话记录").classes("text-h5 font-semibold text-grey-7 mb-2")
|
|
95
|
+
ui.label("开始您的第一次录音会话").classes("text-body2 text-grey-6 mb-6")
|
|
96
|
+
ui.button("创建新会话", on_click=lambda: ui.navigate.to("/recording")).props(
|
|
97
|
+
"color=primary size=lg no-caps"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
with (
|
|
102
|
+
ui.scroll_area().classes("w-full").style("height: calc(100vh - 64px);"),
|
|
103
|
+
(
|
|
104
|
+
ui.element("div")
|
|
105
|
+
.classes("w-full bg-white")
|
|
106
|
+
.style(
|
|
107
|
+
"display: flex; justify-content: center; align-items: flex-start; "
|
|
108
|
+
"min-height: 100%; padding: 60px 80px;"
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
ui.element("div").style(
|
|
112
|
+
"display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; "
|
|
113
|
+
"max-width: 850px; width: 100%;"
|
|
114
|
+
),
|
|
115
|
+
):
|
|
116
|
+
for session in sessions:
|
|
117
|
+
with ui.card().classes("super-card cursor-pointer p-7"):
|
|
118
|
+
with ui.row().classes("items-start justify-between w-full mb-2"):
|
|
119
|
+
ui.label(session.patient_name or "未知患者").classes(
|
|
120
|
+
"text-h6 font-bold text-grey-9"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if session.status.value != "DRAFT":
|
|
124
|
+
status_map = {
|
|
125
|
+
"COMPLETED": ("已完成", "status-completed"),
|
|
126
|
+
"IN_PROGRESS": ("进行中", "status-in-progress"),
|
|
127
|
+
"CANCELLED": ("已取消", "status-cancelled"),
|
|
128
|
+
}
|
|
129
|
+
if session.status.value in status_map:
|
|
130
|
+
status_text, status_class = status_map[session.status.value]
|
|
131
|
+
ui.html(
|
|
132
|
+
f'<div class="status-badge {status_class}">{status_text}</div>',
|
|
133
|
+
sanitize=False,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
with ui.column().classes("gap-2 mb-auto"):
|
|
137
|
+
if session.clinic_number:
|
|
138
|
+
ui.label(f"门诊号: {session.clinic_number}").classes(
|
|
139
|
+
"text-sm text-grey-7"
|
|
140
|
+
)
|
|
141
|
+
if session.medical_record_number:
|
|
142
|
+
ui.label(f"病历号: {session.medical_record_number}").classes(
|
|
143
|
+
"text-sm text-grey-7"
|
|
144
|
+
)
|
|
145
|
+
if session.diagnosis:
|
|
146
|
+
ui.label(f"诊断: {session.diagnosis}").classes("text-sm text-grey-7")
|
|
147
|
+
|
|
148
|
+
time_text = session.created_at.strftime("%m月%d日 %H:%M")
|
|
149
|
+
if session.started_at:
|
|
150
|
+
time_text = session.started_at.strftime("%m月%d日 %H:%M")
|
|
151
|
+
ui.label(time_text).classes("text-sm text-grey-6")
|
|
152
|
+
|
|
153
|
+
with ui.element("div").classes("button-layout"):
|
|
154
|
+
|
|
155
|
+
def create_delete_handler(sid, sname):
|
|
156
|
+
async def handler():
|
|
157
|
+
await show_delete_dialog(sid, sname)
|
|
158
|
+
|
|
159
|
+
return handler
|
|
160
|
+
|
|
161
|
+
ui.button(
|
|
162
|
+
icon="delete_outline",
|
|
163
|
+
on_click=create_delete_handler(
|
|
164
|
+
session.id, session.patient_name or "未知患者"
|
|
165
|
+
),
|
|
166
|
+
).props("flat").classes("btn-delete")
|
|
167
|
+
|
|
168
|
+
with ui.element("div").classes("right-buttons"):
|
|
169
|
+
|
|
170
|
+
def create_view_handler(sid):
|
|
171
|
+
def handler():
|
|
172
|
+
ui.navigate.to(f"/sessions/details?session_id={sid}")
|
|
173
|
+
|
|
174
|
+
return handler
|
|
175
|
+
|
|
176
|
+
ui.button(
|
|
177
|
+
"查看",
|
|
178
|
+
icon="visibility",
|
|
179
|
+
on_click=create_view_handler(session.id),
|
|
180
|
+
).props("outline color=grey-8 no-caps").classes(
|
|
181
|
+
"action-btn btn-secondary"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def create_continue_handler(sid):
|
|
185
|
+
def handler():
|
|
186
|
+
ui.navigate.to(f"/recording?session_id={sid}")
|
|
187
|
+
|
|
188
|
+
return handler
|
|
189
|
+
|
|
190
|
+
ui.button(
|
|
191
|
+
"继续",
|
|
192
|
+
icon="play_arrow",
|
|
193
|
+
on_click=create_continue_handler(session.id),
|
|
194
|
+
).props("unelevated color=primary no-caps").classes(
|
|
195
|
+
"action-btn btn-primary"
|
|
196
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dependency_injector.wiring import Provide
|
|
4
|
+
from dependency_injector.wiring import inject
|
|
5
|
+
from fastapi import Depends
|
|
6
|
+
from fastapi import Query
|
|
7
|
+
from nicegui import ui
|
|
8
|
+
|
|
9
|
+
from audex.container import Container
|
|
10
|
+
from audex.service.session import SessionService
|
|
11
|
+
from audex.service.session.types import UpdateSessionCommand
|
|
12
|
+
from audex.view.decorators import handle_errors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@ui.page("/sessions/details")
|
|
16
|
+
@handle_errors
|
|
17
|
+
@inject
|
|
18
|
+
async def render(
|
|
19
|
+
session_service: SessionService = Depends(Provide[Container.service.session]),
|
|
20
|
+
session_id: str = Query(...),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Render session detail page with left form and right
|
|
23
|
+
conversation."""
|
|
24
|
+
# Add CSS
|
|
25
|
+
ui.add_head_html('<link rel="stylesheet" href="/static/css/sessions/styles.css">')
|
|
26
|
+
|
|
27
|
+
# Fetch session and utterances
|
|
28
|
+
session = await session_service.get(session_id)
|
|
29
|
+
if not session:
|
|
30
|
+
ui.notify("会话不存在", type="negative", position="top")
|
|
31
|
+
ui.navigate.to("/sessions")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
utterances = await session_service.get_utterances(session_id)
|
|
35
|
+
|
|
36
|
+
# State
|
|
37
|
+
is_editing = {"value": False}
|
|
38
|
+
|
|
39
|
+
# Header
|
|
40
|
+
with ui.header().classes("header-glass items-center justify-between px-6 py-3"):
|
|
41
|
+
with ui.row().classes("items-center gap-3"):
|
|
42
|
+
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to("/sessions")).props(
|
|
43
|
+
"flat round"
|
|
44
|
+
).tooltip("返回历史会话")
|
|
45
|
+
ui.label(f"{session.patient_name or '未知患者'}").classes(
|
|
46
|
+
"text-h6 font-semibold text-grey-9"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
ui.button(
|
|
50
|
+
"继续录音",
|
|
51
|
+
icon="mic",
|
|
52
|
+
on_click=lambda: ui.navigate.to(f"/recording?session_id={session_id}"),
|
|
53
|
+
).props("unelevated color=primary no-caps").classes("header-btn")
|
|
54
|
+
|
|
55
|
+
# Main container
|
|
56
|
+
with (
|
|
57
|
+
ui.element("div")
|
|
58
|
+
.classes("w-full bg-white")
|
|
59
|
+
.style("display: flex; padding: 40px 80px; gap: 40px;")
|
|
60
|
+
):
|
|
61
|
+
# Left sidebar: Details
|
|
62
|
+
with ui.column().classes("gap-2").style("width: 450px; flex-shrink: 0;"):
|
|
63
|
+
# Header with edit button
|
|
64
|
+
with ui.row().classes("w-full items-center justify-between mb-6"):
|
|
65
|
+
ui.label("会话信息").classes("text-h4 font-bold text-grey-9")
|
|
66
|
+
|
|
67
|
+
def toggle_edit():
|
|
68
|
+
"""Toggle edit mode."""
|
|
69
|
+
is_editing["value"] = not is_editing["value"]
|
|
70
|
+
|
|
71
|
+
if is_editing["value"]:
|
|
72
|
+
edit_btn.props("icon=close")
|
|
73
|
+
edit_btn.text = "取消"
|
|
74
|
+
info_display.visible = False
|
|
75
|
+
edit_form.visible = True
|
|
76
|
+
else:
|
|
77
|
+
edit_btn.props("icon=edit")
|
|
78
|
+
edit_btn.text = "编辑"
|
|
79
|
+
info_display.visible = True
|
|
80
|
+
edit_form.visible = False
|
|
81
|
+
|
|
82
|
+
edit_btn = (
|
|
83
|
+
ui.button("编辑", icon="edit", on_click=toggle_edit)
|
|
84
|
+
.props("flat no-caps")
|
|
85
|
+
.classes("action-button")
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Info display (read-only)
|
|
89
|
+
info_display = ui.column().classes("w-full gap-0")
|
|
90
|
+
with info_display:
|
|
91
|
+
with ui.element("div").classes("info-field"):
|
|
92
|
+
ui.label("患者姓名").classes("text-xs text-grey-6 mb-1")
|
|
93
|
+
ui.label(session.patient_name or "未填写").classes(
|
|
94
|
+
"text-body1 text-grey-9 font-medium"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with ui.element("div").classes("info-field"):
|
|
98
|
+
ui.label("门诊号").classes("text-xs text-grey-6 mb-1")
|
|
99
|
+
ui.label(session.clinic_number or "未填写").classes(
|
|
100
|
+
"text-body1 text-grey-9 font-medium"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
with ui.element("div").classes("info-field"):
|
|
104
|
+
ui.label("病历号").classes("text-xs text-grey-6 mb-1")
|
|
105
|
+
ui.label(session.medical_record_number or "未填写").classes(
|
|
106
|
+
"text-body1 text-grey-9 font-medium"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
with ui.element("div").classes("info-field"):
|
|
110
|
+
ui.label("诊断").classes("text-xs text-grey-6 mb-1")
|
|
111
|
+
ui.label(session.diagnosis or "未填写").classes(
|
|
112
|
+
"text-body1 text-grey-9 font-medium"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if session.notes:
|
|
116
|
+
with ui.element("div").classes("info-field"):
|
|
117
|
+
ui.label("备注").classes("text-xs text-grey-6 mb-1")
|
|
118
|
+
ui.label(session.notes).classes("text-body1 text-grey-9 font-medium")
|
|
119
|
+
|
|
120
|
+
# Edit form - 复用 recording 的模态框样式
|
|
121
|
+
edit_form = ui.column().classes("w-full gap-4")
|
|
122
|
+
edit_form.visible = False
|
|
123
|
+
with edit_form:
|
|
124
|
+
with ui.row().classes("w-full gap-4"):
|
|
125
|
+
patient_name_input = (
|
|
126
|
+
ui.input("", placeholder="患者姓名")
|
|
127
|
+
.classes("flex-1 clean-input")
|
|
128
|
+
.props("standout dense hide-bottom-space")
|
|
129
|
+
)
|
|
130
|
+
patient_name_input.value = session.patient_name or ""
|
|
131
|
+
|
|
132
|
+
clinic_number_input = (
|
|
133
|
+
ui.input("", placeholder="门诊号")
|
|
134
|
+
.classes("flex-1 clean-input")
|
|
135
|
+
.props("standout dense hide-bottom-space")
|
|
136
|
+
)
|
|
137
|
+
clinic_number_input.value = session.clinic_number or ""
|
|
138
|
+
|
|
139
|
+
with ui.row().classes("w-full gap-4 mt-3"):
|
|
140
|
+
medical_record_number_input = (
|
|
141
|
+
ui.input("", placeholder="病历号")
|
|
142
|
+
.classes("flex-1 clean-input")
|
|
143
|
+
.props("standout dense hide-bottom-space")
|
|
144
|
+
)
|
|
145
|
+
medical_record_number_input.value = session.medical_record_number or ""
|
|
146
|
+
|
|
147
|
+
diagnosis_input = (
|
|
148
|
+
ui.input("", placeholder="诊断")
|
|
149
|
+
.classes("flex-1 clean-input")
|
|
150
|
+
.props("standout dense hide-bottom-space")
|
|
151
|
+
)
|
|
152
|
+
diagnosis_input.value = session.diagnosis or ""
|
|
153
|
+
|
|
154
|
+
notes_input = (
|
|
155
|
+
ui.textarea("", placeholder="备注")
|
|
156
|
+
.classes("w-full mt-3 clean-input notes-textarea")
|
|
157
|
+
.props("standout hide-bottom-space")
|
|
158
|
+
)
|
|
159
|
+
notes_input.value = session.notes or ""
|
|
160
|
+
|
|
161
|
+
@handle_errors
|
|
162
|
+
async def save_session():
|
|
163
|
+
"""Save session changes."""
|
|
164
|
+
await session_service.update(
|
|
165
|
+
UpdateSessionCommand(
|
|
166
|
+
session_id=session_id,
|
|
167
|
+
patient_name=patient_name_input.value.strip() or None,
|
|
168
|
+
clinic_number=clinic_number_input.value.strip() or None,
|
|
169
|
+
medical_record_number=medical_record_number_input.value.strip() or None,
|
|
170
|
+
diagnosis=diagnosis_input.value.strip() or None,
|
|
171
|
+
notes=notes_input.value.strip() or None,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
ui.notify("会话信息已更新", type="positive", position="top")
|
|
176
|
+
ui.navigate.to(f"/sessions/details?session_id={session_id}")
|
|
177
|
+
|
|
178
|
+
ui.button("保存更改", on_click=save_session).props(
|
|
179
|
+
"unelevated color=primary size=lg no-caps"
|
|
180
|
+
).classes("action-button").style("height: 48px;")
|
|
181
|
+
|
|
182
|
+
# Right content area: Utterances (scrollable)
|
|
183
|
+
content_scroll = (
|
|
184
|
+
ui.scroll_area()
|
|
185
|
+
.classes("flex-1")
|
|
186
|
+
.style("height: calc(100vh - 190px); padding: 0 20px;")
|
|
187
|
+
)
|
|
188
|
+
with content_scroll:
|
|
189
|
+
if not utterances:
|
|
190
|
+
with (
|
|
191
|
+
ui.element("div")
|
|
192
|
+
.classes("empty-content")
|
|
193
|
+
.style(
|
|
194
|
+
"height: 100%; display: flex; align-items: center; justify-content: center;"
|
|
195
|
+
),
|
|
196
|
+
ui.column().classes("items-center"),
|
|
197
|
+
):
|
|
198
|
+
ui.icon("chat_bubble_outline", size="3xl").classes("text-grey-4 mb-4")
|
|
199
|
+
ui.label("暂无对话记录").classes("text-lg text-grey-6")
|
|
200
|
+
else:
|
|
201
|
+
with ui.element("div").classes("utterances-container"):
|
|
202
|
+
for utterance in utterances:
|
|
203
|
+
speaker_name = "医生" if utterance.is_doctor else "患者"
|
|
204
|
+
time_str = utterance.timestamp.strftime("%H:%M:%S")
|
|
205
|
+
duration = f"{utterance.duration_ms / 1000:.1f}s"
|
|
206
|
+
|
|
207
|
+
if utterance.is_doctor:
|
|
208
|
+
with ui.element("div").classes("utterance-final is-doctor"):
|
|
209
|
+
ui.label(f"{speaker_name} • {time_str} • {duration}").classes(
|
|
210
|
+
"speaker-label"
|
|
211
|
+
)
|
|
212
|
+
ui.html(
|
|
213
|
+
f'<div class="bubble-doctor">{utterance.text}</div>',
|
|
214
|
+
sanitize=False,
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
with ui.element("div").classes("utterance-final is-patient"):
|
|
218
|
+
ui.label(f"{speaker_name} • {time_str} • {duration}").classes(
|
|
219
|
+
"speaker-label"
|
|
220
|
+
)
|
|
221
|
+
ui.html(
|
|
222
|
+
f'<div class="bubble-patient">{utterance.text}</div>',
|
|
223
|
+
sanitize=False,
|
|
224
|
+
)
|