6.3 KiB
6.3 KiB
2026-08-20 — 医生工作站 AI 问诊对话框布局优化
改动
- 文件:
app/src/doctor_workstation/ui/dialogs/ai_consult.py - 模块:
AiConsultDialog右侧「智能分析」卡片 + 问诊对话卡片密度优化 - 新增 helpers:
_InsightItem,_split_insight_segments,_split_numbered_list,_split_plain_paragraph,_split_label_or_sentence,_escape_html _set_insights改成将稠密段落拆成 lead / list / plain 三类, 每类单独 QLabel 显示- CSS 新增
AiConsultInsightMarker/AiConsultInsightText/AiConsultInsightPlaceholder - 卡片 padding
10,8,10,8→14,12,14,14; 卡间距8→10 - 聊天气泡垂直 margin
4→6, chat_layout spacing10→12
拆分策略
- 文本以
1./2、/3)/4)起头 → 视为完整编号列表 - 文本中嵌入
…:1.…2.…→ 拆出序言 + 编号列表 - 短中文标签
XX:YY开头 → 视为 lead, 标签部分加粗 - 其余按
。/;/;/分句 → 单独成行
测试
- 现有
tests/test_ai_consult_ui.py通过widget.objectName() == "AiConsultRecordTitle"校验标题仍在, 未受影响 - 暂未运行 UI 测试 (开发环境未装 PySide6); 解析逻辑单独脚本验证通过
注意
- PySide6 QSS 对
QLabel的line-height支持有限; 主要靠 item 间距8px与padding:1px 0提供节奏 - 如果后续接入
AiConsultInsightText还嫌密集, 可再调body_host.setSpacing
接诊台 AI 智能分析详情对话框 (ReceptionAiAnalysisDialog) 优化
用户截图(clipboard-2026-08-20T08-42-40...)反馈: 「问诊详情 → AI 智能分析」卡片里的诊断建议 / 风险评估 / 治疗建议三段文字太稠密, 不利于阅读。
关键差异
这次的目标文件和上次不同 — 用户截图里出现 "诊断建议 / 风险评估 / 治疗建议" 标题, 而 ai_consult.py 用的是 "血糖控制评估 / 并发症风险评估 / 用药合理性评估"。实际是 app/src/doctor_workstation/ui/pages/reception.py 中的 _ReceptionAiAnalysisDialog 类。
改动
- 文件:
app/src/doctor_workstation/ui/pages/reception.py - 新增 helpers (与 ai_consult.py 类似但导出名加
_ai_前缀):_ai_split_into_segments/_ai_split_numbered_list/_ai_split_plain_paragraph_ai_segments_to_html/_ai_narrative_structured_html_ai_split_label_or_sentence/_ai_escape_html- 正则:
_AI_INSIGHT_LIST_RE,_AI_INSIGHT_LIST_LOOKAHEAD_RE,_AI_INSIGHT_LABEL_RE
add_text_section闭包内多挂一个 RichText_structured_labels[name]标签 (.setObjectName(name + "Structured"),property="dialogAiStructured")- 原 PlainText
value_label仍存在 (hide()+setMaximumHeight(0)) 用于兼容既有 findChild 测试 _render_model同步刷新两个结构化标签; 空 HTML 时回退显示原 label 文本- 风险评估 FlowLayout spacing
7×7→9×9 body_layout.setSpacing12→14- 卡片 contentsMargins
14,13,16,15→16,14,18,16 - QSS:
dialogAiBody加line-height: 1.85, 新增dialogAiStructured选择器 (line-height 175%); 风险 pillmin-height28→30, padding4 10→5 12, font 12 → 12.5, border-radius7→8
兼容约束
tests/test_reception_parity_ui.py严格校验:findChild(QLabel, "ReceptionAiAnalysisDialogDiagnosisText")必须能找到.text()必须等于_ai_narrative_text归一化结果.textFormat() == Qt.TextFormat.PlainText
- 因此 PlainText 标签必须原样保留, 仅做
.hide()+setMaximumHeight(0)(避免 QSS>子选择器不被 Qt 支持)
验证
py_compile通过, 无SyntaxWarning- 单独脚本跑过 4 组样本文本 (稠密段落 / 嵌入式编号列表 / 短句 / 纯编号列表), 拆分结果符合预期
- 未在本机跑 UI 测试 (缺 PySide6)
患者列表 AI 分析: 发送完整患者资料给 AI
用户需求: 患者列表 AI 分析时, 每日血糖报告、舌苔、AI 报告、视频录制回放的文字对话都要发给 AI。
改动
app/src/doctor_workstation/ui/dialogs/ai_consult.py_load_workspace新增list_call_recordssection (视频问诊记录, 含transcript_text)- 模块级纯函数 (无 Qt 依赖, 便于测试):
AI_CONTEXT_MAX_CHARS=320,AI_PROMPT_LIMIT=500,AI_CONTEXT_SEPARATOR="\n\n— 医生提问 —\n"_truncate_for_context/_patient_context_blood_sugar/_patient_context_tongue/_patient_context_reports/_patient_context_prescriptions/_patient_context_videos/build_patient_ai_context/_compose_ai_prompt
build_patient_ai_context返回(text, labels), 按 每日血糖→舌苔/脉象→视频问诊文字→历史AI报告→处方记录 顺序装入 320 字预算的信封_patient_context_videos按diagnosis_id过滤归属 (_exact_positive_id), 只取最近一条有文字的_compose_ai_prompt本地强制 500 字上限: 上下文 + "— 医生提问 —" + 问题, 超长时截断问题加…(服务端tcm.diagnosis/aiAssistantprompt 上限 500)_ChatBubble.attach_extra()支持气泡下方挂载控件_render_patient_context: 聊天区系统气泡 "AI 已加载本诊单上下文(N 项…)",_attach_context_detail挂 "查看本次发送给 AI 的资料全文" 展开按钮 (QSS:AiConsultContextToggle/AiConsultContextReveal)_ask用_compose_ai_prompt(text, self._patient_ai_context)组装后传给_AiStreamWorker; 医生气泡仍显示原始问题open_for/_clear_chat重置_patient_ai_context*三个状态, 防止跨患者残留
测试
app/tests/test_ai_consult_ui.py新增 5 个测试 (总 15 个全通过): 纯函数: 全 section 信封+视频按诊单过滤 / 仅基本病历返回空 / 长问题截断≤500 UI: 上下文气泡+展开按钮+视频记录计数 /_ask注入上下文 (FakeWorker 捕获 prompt, monkeypatchQThreadPool)app/scripts/check_ai_context.py: AST 提取纯函数的冒烟脚本 (无需 Qt)tests/test_ai_consult_workspace_ui.py18 个测试无回归
环境要点 (重要)
- PySide6 + pytest 在
app/.venv里可用: 用D:/web/zyt/app/.venv/Scripts/python.exe -m pytest跑 UI 测试 (QT_QPA_PLATFORM=offscreen)。之前"本机缺 PySide6"的结论只对系统 Python 成立。 - 测试 FakeWorker 需实现
signals.event/error/finished.connect()和cancel()(closeEvent 会调 cancel)