import {
applyCandidateSelectionResponseFixture,
applyCommandProjectionFixture,
applyReportInterpretationProjectionFixture,
applyServerProjectionFixture,
buildCandidateSelectionInteraction,
buildCommandConfirmationRequest,
buildCommandRejectionRequest,
closeReportView,
createInitialState,
evaluateReportInterpretationGate,
grantConsent,
hasValidReportConsent,
loadScenario,
openReportDetails,
rejectConsent,
requestReportInterpretation,
revokeConsent,
selectHomeTasks,
selectReports,
selectValidReportResult,
submitReportFeedback,
submitInteraction,
} from "./patient-service-desk-v3.state.mjs";
export const navItems = Object.freeze([
{ id: "home", label: "首页", icon: "house" },
{ id: "care", label: "就医", icon: "stethoscope" },
{ id: "assistant", label: "AI助手", icon: "sparkles" },
{ id: "records", label: "健康档案", icon: "folder-heart" },
{ id: "profile", label: "我的", icon: "user-round" },
]);
export const demoScenarioGroups = Object.freeze({
REPORT_CLOSURE: Object.freeze([
"REPORT_ARRIVED",
"REPORT_DETAIL",
"REPORT_CONSENT",
"REPORT_GENERATING",
"REPORT_READY",
"REPORT_NEED_REVIEW",
"REPORT_CRITICAL",
"REPORT_INVALIDATED",
]),
CARE_CLOSURE: Object.freeze([
"EMPTY_HOME",
"REGISTRATION_QUERY",
"COMMAND_PREPARED",
"APPOINTMENT_CREATED",
"CHECKIN_AVAILABLE",
"QUEUE_UPDATED",
]),
});
export const demoScenarios = Object.freeze(
Object.values(demoScenarioGroups).flat(),
);
const scenarioLabels = {
EMPTY_HOME: "空闲首页",
REPORT_ARRIVED: "报告到达",
REPORT_DETAIL: "报告详情",
REPORT_CONSENT: "报告授权",
REPORT_GENERATING: "解读生成中",
REPORT_READY: "报告已解读",
REPORT_NEED_REVIEW: "报告需复核",
REPORT_CRITICAL: "报告危急值",
REPORT_INVALIDATED: "报告已失效",
REGISTRATION_QUERY: "自然语言查号",
COMMAND_PREPARED: "挂号待确认",
APPOINTMENT_CREATED: "挂号成功",
CHECKIN_AVAILABLE: "到院可签到",
QUEUE_UPDATED: "候诊更新",
};
const homeServices = Object.freeze([
{ label: "预约挂号", icon: "calendar-plus" },
{ label: "智能导诊", icon: "scan-heart" },
{ label: "报告查询", icon: "file-chart-column" },
{ label: "门诊缴费", icon: "wallet-cards" },
{ label: "检查预约", icon: "clipboard-clock" },
{ label: "住院服务", icon: "bed" },
{ label: "院内导航", icon: "map-pinned" },
{ label: "在线问诊", icon: "messages-square" },
]);
const actionLabels = {
INTERPRET_REPORT: "AI 解读报告",
VIEW_REPORT: "查看原报告",
CONFIRM_CHECKIN: "确认签到",
VIEW_ROUTE: "查看路线",
VIEW_QUEUE: "查看候诊",
};
const actionTokens = {
INTERPRET_REPORT: "interpret-report",
VIEW_REPORT: "view-report",
CONFIRM_CHECKIN: "open-checkin-confirmation",
VIEW_ROUTE: "open-route",
VIEW_QUEUE: "open-queue",
};
const statusPresentation = {
ACTION_REQUIRED: {
className: "action-required",
icon: "circle-alert",
label: "需要处理",
},
IN_PROGRESS: {
className: "in-progress",
icon: "loader-circle",
label: "进行中",
},
UPCOMING: {
className: "upcoming",
icon: "calendar-clock",
label: "即将开始",
},
COMPLETED: {
className: "completed",
icon: "circle-check-big",
label: "已完成",
},
};
const unknownStatusPresentation = {
className: "unknown",
icon: "circle-help",
label: "状态待确认",
};
const escapeHtml = (value) => String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const getActionLabel = (action) => (
typeof action === "string" && Object.hasOwn(actionLabels, action)
? actionLabels[action]
: null
);
const renderActionAttributes = (action, task) => {
const token = typeof action === "string" && Object.hasOwn(actionTokens, action)
? actionTokens[action]
: null;
if (!token) {
return "";
}
if (typeof task?.reportId === "string") {
return ` data-action="${token}" data-report-id="${escapeHtml(task.reportId)}"`;
}
if (typeof task?.appointmentId === "string") {
return ` data-action="${token}" data-appointment-id="${escapeHtml(task.appointmentId)}"`;
}
return "";
};
const getStatusPresentation = (status) => (
typeof status === "string" && Object.hasOwn(statusPresentation, status)
? statusPresentation[status]
: unknownStatusPresentation
);
const getShellElements = (root) => {
if (!root?.getElementById) {
return null;
}
const appHeader = root.getElementById("appHeader");
const appView = root.getElementById("appView");
const bottomNav = root.getElementById("bottomNav");
return appHeader && appView && bottomNav
? { appHeader, appView, bottomNav }
: null;
};
const renderIcons = () => {
const lucide = globalThis.lucide;
if (lucide?.createIcons) {
lucide.createIcons({ attrs: { "aria-hidden": "true" } });
}
};
const patientDisplayNames = {
"patient-zhang-san": "张先生",
"patient-li-na": "李女士",
};
const patientDisplayName = (state) => (
patientDisplayNames[state?.activePatientId] ?? "当前就诊人"
);
const renderHeader = (state) => {
const messages = Array.isArray(state?.messages) ? state.messages : [];
const unreadCount = messages.filter((message) => message?.unread).length;
const messageLabel = unreadCount > 0
? `消息,${unreadCount} 条未读`
: "消息";
return `
`;
};
const journeyStages = [
{ stage: "WAITING_ARRIVAL", label: "到院", time: "09:10" },
{ stage: "CHECKIN_AVAILABLE", label: "签到", time: "09:20" },
{ stage: "QUEUE_WAITING", label: "候诊", time: "09:30" },
{ stage: "VISIT_IN_PROGRESS", label: "医生接诊", time: "10:05" },
{ stage: "POST_VISIT", label: "诊后服务", time: "10:25" },
];
const renderJourneyProjection = (journey) => {
const activeIndex = Math.max(
0,
journeyStages.findIndex(({ stage }) => stage === journey.stage),
);
const start = Math.max(
0,
Math.min(activeIndex - 1, journeyStages.length - 3),
);
const visibleStages = journeyStages.slice(start, start + 3);
return `
${escapeHtml(journey.title)}
${escapeHtml(journey.location ?? journey.room ?? "院内行程持续更新")}
${journey.stage === "QUEUE_WAITING" ? `
` : ""}
${visibleStages.map((node) => `
-
${node.label}
`).join("")}
`;
};
export const renderHome = (state) => {
const tasks = selectHomeTasks(state);
const journeys = Array.isArray(state?.journeyProjections)
? state.journeyProjections
: [];
const primaryTaskIndex = tasks.findIndex(
({ primaryAction, status }) => (
status === "ACTION_REQUIRED" || status === "IN_PROGRESS"
) && Boolean(getActionLabel(primaryAction)),
);
const activeTaskCount = tasks.filter(
({ status }) => status === "ACTION_REQUIRED" || status === "IN_PROGRESS",
).length;
return `
AI 就医助手
${patientDisplayName(state)},上午好
${activeTaskCount > 0 ? `有 ${activeTaskCount} 项就医事项需要关注` : "今天暂无待处理的就医事项"}
${tasks.length === 0 ? `
今天没有待处理事项
新的报告、缴费或预约提醒会显示在这里。
` : tasks.map((task, index) => {
const isCompleted = task.status === "COMPLETED";
const status = getStatusPresentation(task.status);
const isPrimaryTask = index === primaryTaskIndex;
const primaryActionLabel = getActionLabel(task.primaryAction);
const secondaryActionLabel = getActionLabel(task.secondaryAction);
const canAct = (
task.status === "ACTION_REQUIRED"
|| task.status === "IN_PROGRESS"
) && Boolean(primaryActionLabel);
const recommendation = isCompleted
? "无需操作"
: canAct
? primaryActionLabel
: task.status === "UPCOMING"
? "等待事项开始"
: "确认状态与可用操作";
return `
${status.label}
${escapeHtml(task.title)}
发生了什么:${escapeHtml(task.description)}
为什么关注:${isCompleted ? "该事项已处理完成。" : "及时处理有助于顺利推进后续就医安排。"}
建议操作
${recommendation}
${canAct ? `
${secondaryActionLabel ? `
` : ""}
` : ""}
`;
}).join("")}
${journeys.length > 0 ? journeys.map(renderJourneyProjection).join("") : `
今天暂无院内行程
挂号或检查安排确认后,将展示前后步骤和当前节点。
`}
${homeServices.map(({ label, icon }) => `
`).join("")}
暂无新的主动关怀提醒
用药、复诊与检查准备提醒会在确认后展示。
`;
};
const candidateToken = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const renderAssistantMessages = (messages) => (
Array.isArray(messages)
? messages
.filter((message) => (
message
&& typeof message === "object"
&& (message.role === "user" || message.role === "assistant")
))
.map(({ role, text }) => `
${role === "user" ? "你" : "AI 助手"}
${escapeHtml(text)}
`).join("")
: ""
);
export const renderAssistant = (state) => {
const task = state?.activeAgentTaskProjection;
const result = typeof task?.activeResultRef === "string"
? state?.resultSnapshotProjections?.[task.activeResultRef]
: null;
const items = task?.stage === "CANDIDATE_SELECTION"
&& result?.resultRef === task.activeResultRef
&& Array.isArray(result?.items)
? result.items
: [];
const candidates = items.filter((item) => (
item
&& typeof item === "object"
&& typeof item.candidateId === "string"
&& candidateToken.test(item.candidateId)
));
const hasTask = task?.type === "REGISTRATION";
return `
场景建议
${renderAssistantMessages(state?.messages)}
${task?.stage === "REQUERY_REQUIRED" ? `
` : ""}
${result ? `
可选号源
${escapeHtml(result.text)}
${candidates.length > 0 ? `
${candidates.map((item) => `
`).join("")}
` : ""}
` : ""}
${task?.stage === "AWAITING_REGISTRATION_CONFIRMATION" ? `
` : ""}
`;
};
const reportFamilyLabels = { LAB: "检验报告", EXAM: "检查报告" };
const reportSourceStatusLabels = {
FINAL: "已审核",
PRELIMINARY: "待审核",
};
const reportInterpretationLabels = {
NOT_REQUESTED: "尚未进行 AI 辅助解读",
INTERPRETING: "AI 辅助解读中",
READY: "AI 辅助解读已生成",
NEED_REVIEW: "解读结果建议由医生确认",
};
const metricFlagLabels = { HIGH: "偏高", LOW: "偏低", NORMAL: "正常" };
const abnormalMetricsOf = (report) => (
Array.isArray(report?.metrics)
? report.metrics.filter((metric) => ["HIGH", "LOW"].includes(metric?.sourceFlag))
: []
);
const renderMetric = (metric) => {
const name = escapeHtml(metric?.name || "未命名指标");
const value = metric?.value === null || metric?.value === undefined
? "—"
: escapeHtml(metric.value);
const unit = metric?.unit ? ` ${escapeHtml(metric.unit)}` : "";
const range = metric?.referenceRange
? `参考范围 ${escapeHtml(metric.referenceRange)}${unit}`
: "参考范围未提供";
const flag = metricFlagLabels[metric?.sourceFlag] ?? "结果待确认";
return `
${name}${value}${unit}${range}${flag}
`;
};
export const renderReportCenter = (state) => {
const filters = {
family: ["ALL", "LAB", "EXAM"].includes(state?.reportFilters?.family)
? state.reportFilters.family : "ALL",
status: ["ALL", "UNREAD", "READ"].includes(state?.reportFilters?.status)
? state.reportFilters.status : "ALL",
};
const reports = selectReports(state, filters);
return `
${pageHeader("健康档案", "报告中心", "医院原始报告和 AI 辅助解读统一查看。")}
当前就诊人 ${escapeHtml(patientDisplayName(state))}
${[["ALL", "全部"], ["LAB", "检验报告"], ["EXAM", "检查报告"]].map(([id, label]) => `
`).join("")}
${[["ALL", "全部"], ["UNREAD", "未读"], ["READ", "已读"]].map(([id, label]) => `
`).join("")}
${reports.length ? reports.map((report) => {
const abnormalCount = abnormalMetricsOf(report).length;
return `
`;
}).join("") : `
暂无符合筛选条件的报告可以调整报告类型或阅读状态。
`}
`;
};
export const renderReportDetail = (report = {}, expanded = false) => {
const metrics = Array.isArray(report?.metrics) ? report.metrics : [];
const abnormalMetrics = abnormalMetricsOf(report);
const hiddenMetrics = metrics.filter(
(metric) => !abnormalMetrics.includes(metric),
);
const visibleMetrics = expanded
? [...abnormalMetrics, ...hiddenMetrics]
: abnormalMetrics;
const reportId = report?.reportId ?? "";
return `
医院原始报告
${escapeHtml(report?.title || "未命名报告")}
${escapeHtml(report?.hospitalName || "医院信息待确认")} · ${escapeHtml(report?.departmentName || "科室待确认")}
出具时间:${escapeHtml(report?.issuedAt || "待确认")}
${escapeHtml(reportSourceStatusLabels[report?.sourceStatus] ?? "报告状态待确认")}
AI 辅助解读仅供参考,报告结果需结合症状、病史并由医生判断。
指标概览
${abnormalMetrics.length ? `${abnormalMetrics.length} 项异常指标` : "未标记异常指标"}
${visibleMetrics.length ? visibleMetrics.map(renderMetric).join("") : "- 暂无指标数据
"}
${hiddenMetrics.length ? `
` : ""}
`;
};
const safeReportText = (value, fallback) => (
typeof value === "string" && value.trim()
? escapeHtml(value.trim())
: fallback
);
export const renderExaminationReportDetail = (report = {}) => {
const reportId = typeof report?.reportId === "string"
? report.reportId
: "";
return `
医院检查报告
${safeReportText(report?.title, "检查名称待确认")}
检查部位:${safeReportText(report?.bodyPart, "待确认")}
${safeReportText(report?.hospitalName, "医院信息待确认")} · ${safeReportText(report?.departmentName, "科室待确认")}
出具时间:${safeReportText(report?.issuedAt, "待确认")}
检查所见
检查所见
${safeReportText(report?.findings, "检查所见待确认")}
报告结论
报告结论
${safeReportText(report?.conclusion, "报告结论待确认")}
报告医生:${safeReportText(report?.reportingDoctor, "报告医生待确认")}
审核状态:${safeReportText(
report?.reviewStatus,
reportSourceStatusLabels[report?.sourceStatus] ?? "审核状态待确认",
)}
`;
};
export const renderOriginalReport = (report = {}) => {
if (report?.family === "EXAM") {
return `
医院检查报告原件
${safeReportText(report?.title, "检查名称待确认")}
${safeReportText(report?.originalFormat, "图像或PDF")}
原始图像或PDF由医院报告系统提供,本原型展示统一查看入口。
`;
}
const metrics = Array.isArray(report?.metrics) ? report.metrics : [];
return `
医院原始报告
原始报告内容
${escapeHtml(report?.title || "未命名报告")}
${escapeHtml(report?.hospitalName || "医院信息待确认")} · ${escapeHtml(report?.departmentName || "科室待确认")}
出具时间:${escapeHtml(report?.issuedAt || "待确认")}
${metrics.length ? metrics.map(renderMetric).join("") : '- 暂无指标数据
'}
`;
};
export const renderConsentOverlay = (state = {}) => {
const reportId = state?.overlays?.reportId;
const report = state?.reportSourceProjections?.[reportId];
return `
隐私授权
授权 AI 辅助解释本次报告
- 就诊人
- ${escapeHtml(patientDisplayName(state))}
- 用途
- 仅用于这份报告的 AI 辅助解释
- 范围
- ${escapeHtml(report?.title || "当前血常规报告")}
- 有效期
- 至 2026-07-09 23:59
AI 辅助性质:用于帮助理解指标,不替代医生诊断或诊疗建议。
风险说明:解释可能存在遗漏或偏差,请结合症状由医生判断。
撤回方式:可随时在隐私设置中撤回,撤回后不再展示历史解释。
`;
};
const commandPresentation = {
PREPARED: { label: "准备中", icon: "loader-circle", tone: "pending" },
AWAITING_CONFIRMATION: { label: "待确认", icon: "shield-check", tone: "warning" },
EXECUTING: { label: "执行中", icon: "loader-circle", tone: "pending" },
SUCCEEDED: { label: "挂号成功", icon: "circle-check-big", tone: "success" },
FAILED: { label: "执行失败", icon: "circle-x", tone: "danger" },
EXPIRED: { label: "已过期", icon: "clock-alert", tone: "danger" },
UNKNOWN: { label: "结果确认中,请勿重复操作", icon: "circle-help", tone: "warning" },
REJECTED: { label: "已暂不操作", icon: "circle-minus", tone: "muted" },
};
export const renderCommandOverlay = (command) => {
const presentation = commandPresentation[command?.status]
?? commandPresentation.PREPARED;
const details = command?.details ?? {};
const statusLabel = command?.status === "SUCCEEDED" && !details.patientName
? "模拟完成"
: presentation.label;
const awaiting = command?.status === "AWAITING_CONFIRMATION";
const canRequery = command?.status === "EXPIRED"
|| command?.status === "FAILED";
const canClose = command?.status === "SUCCEEDED"
|| command?.status === "REJECTED";
return `
${statusLabel}
受控业务操作
${escapeHtml(details.operationName ?? "确认挂号")}
- 就诊人
- ${escapeHtml(details.patientName)}
- 科室 / 医生
- ${escapeHtml(details.departmentName)} · ${escapeHtml(details.doctorName)}${escapeHtml(details.doctorTitle)}
- 就诊时间
- ${escapeHtml(details.visitAt)}
- 金额
- ${escapeHtml(details.fee)}
- 有效时间
- ${escapeHtml(details.countdown ?? details.expiresAt)}
${command?.status === "UNKNOWN" ? `
结果确认中,请勿重复操作。系统会继续查询最终结果。
` : ""}
${command?.status === "FAILED" ? `
本次操作未成功。请重新查询号源,不直接重试该 Command。
` : ""}
${awaiting ? `
` : ""}
${canRequery ? `
` : ""}
${canClose ? `
` : ""}
`;
};
export const renderReportInterpreting = () => `
AI 辅助解释
正在整理报告解读
正在结合参考范围整理便于理解的信息,请稍候。结果仅供辅助理解,不代表诊断结论。
分析仍在进行,暂不提供确定性结论
`;
const reportUnavailablePresentation = Object.freeze({
OCR_REVIEW_REQUIRED: Object.freeze({
title: "当前报告暂不适合自动解读",
description: "报告内容需要人工核对,您仍可查看医院原始报告。",
actions: Object.freeze([
Object.freeze({
action: "view-original-report",
label: "查看原始报告",
tone: "primary",
}),
Object.freeze({
action: "contact-human-service",
label: "联系人工服务",
tone: "secondary",
}),
]),
}),
CRITICAL_NOTICE: Object.freeze({
title: "报告含医院危急提示",
description: "请立即按医院通知联系医务人员,并优先遵循医院既有处置流程。",
actions: Object.freeze([
Object.freeze({
action: "contact-human-service",
label: "立即联系人工服务",
tone: "primary",
}),
Object.freeze({
action: "view-original-report",
label: "查看原始报告",
tone: "secondary",
}),
]),
}),
AI_UNAVAILABLE: Object.freeze({
title: "AI辅助解读暂不可用",
description: "医院原始报告仍可正常查看,请稍后重试。",
}),
REPORT_INVALIDATED: Object.freeze({
title: "医院已更新或撤回报告",
description: "原AI辅助解释已失效。最新版本尚未同步完成,请返回报告中心查看状态。",
actions: Object.freeze([
Object.freeze({
action: "open-report-center",
label: "查看报告状态",
tone: "primary",
}),
Object.freeze({
action: "contact-human-service",
label: "联系人工服务",
tone: "secondary",
}),
]),
}),
UNSUPPORTED: Object.freeze({
title: "暂不支持自动解读",
description: "您可以查看原始报告或联系人工服务。",
}),
});
export const renderReportUnavailable = ({
reason,
reportId,
serviceNotice,
} = {}) => {
const presentation = Object.hasOwn(reportUnavailablePresentation, reason)
? reportUnavailablePresentation[reason]
: reportUnavailablePresentation.UNSUPPORTED;
const safeReportId = typeof reportId === "string"
&& /^[A-Za-z0-9._-]+$/.test(reportId)
? reportId
: "";
const actions = presentation.actions ?? [
{
action: "view-original-report",
label: presentation.primaryActionLabel ?? "查看原始报告",
tone: "primary",
},
{
action: "contact-human-service",
label: "联系人工服务",
tone: "secondary",
},
];
return `
${escapeHtml(presentation.title)}
${escapeHtml(presentation.description)}
${typeof serviceNotice === "string" && serviceNotice ? `
${escapeHtml(serviceNotice)}
` : ""}
${actions.map(({ action, label, tone }) => `
`).join("")}
`;
};
export const renderReportServiceOverlay = (state = {}) => {
const reason = state?.overlays?.reportUnavailableReason;
const presentation = Object.hasOwn(reportUnavailablePresentation, reason)
? reportUnavailablePresentation[reason]
: reportUnavailablePresentation.UNSUPPORTED;
return `
人工协助
人工服务窗口
${escapeHtml(presentation.title)}
${escapeHtml(presentation.description)}
医院服务人员可协助核对报告状态和原始内容。
本原型仅演示服务入口,不会真实发起电话或在线会话。
`;
};
export const renderReportInterpretationResult = (result, options = {}) => {
const interpretation = result?.interpretation;
if (
!interpretation
|| typeof interpretation !== "object"
|| Array.isArray(interpretation)
) {
return "";
}
const highlights = Array.isArray(interpretation.highlights)
? interpretation.highlights
: [];
const explanations = Array.isArray(interpretation.explanations)
? interpretation.explanations
: [];
const commonFactors = Array.isArray(interpretation.commonFactors)
? interpretation.commonFactors
: explanations.map((item) => item?.commonFactors).filter(Boolean);
const canDo = Array.isArray(interpretation.canDo)
? interpretation.canDo
: [];
const evidence = Array.isArray(interpretation.evidence)
? interpretation.evidence
: [];
const reportId = typeof result?.reportId === "string" ? result.reportId : "";
const reportAction = (action) => (
`data-action="${action}" data-report-id="${escapeHtml(reportId)}"`
);
const serviceNotice = typeof options?.serviceNotice === "string"
? options.serviceNotice
: "";
const feedbackCategories = [
["HELPFUL", "有帮助"],
["METRIC_INACCURATE", "指标解释不准确"],
["HARD_TO_UNDERSTAND", "内容难理解"],
["DIFFERS_FROM_DOCTOR", "与医生说明不一致"],
["OTHER", "其他问题"],
];
return `
AI 辅助解释 · 结果版本 ${escapeHtml(result.resultVersion)}
结果概括
${escapeHtml(interpretation.headline)}
${escapeHtml(interpretation.summary)}
安全提示
${escapeHtml(interpretation.safety)}
需要关注
${highlights.map((item) => `
-
${escapeHtml(item?.name)}
${escapeHtml(item?.value)}
${escapeHtml(item?.flag)}
`).join("")}
指标解释
${explanations.map((item) => `
${escapeHtml(item.indicator)}
${escapeHtml(item.explanation)}
`).join("")}
常见相关因素
${commonFactors.map((factor) => `- ${escapeHtml(factor)}
`).join("")}
上述因素只是帮助理解指标的常见情况,不代表你患有相应疾病,也不能作为诊断结果。
可以怎么做
${canDo.map((action) => `- ${escapeHtml(action)}
`).join("")}
内容依据
${evidence.map((item) => `- ${escapeHtml(item)}
`).join("")}
结果版本 ${escapeHtml(result.resultVersion)} · 对应当前医院原始报告
${reportId ? `
需要进一步帮助
${serviceNotice ? `
${escapeHtml(serviceNotice)}
` : ""}
` : ""}
${reportId && options?.feedbackOpen ? `
这次解释对你有帮助吗?
请选择最符合的一项,反馈仅用于改进本次辅助解释。
${feedbackCategories.map(([category, label]) => `
`).join("")}
` : ""}
${options?.feedbackSubmitted ? `
感谢反馈,已记录你的选择。
` : ""}
`;
};
export const renderQueueStatus = (state, appointmentId) => {
const queue = (Array.isArray(state?.patientWorkItems)
? state.patientWorkItems
: []).find((item) => (
item?.type === "WAITING_QUEUE"
&& item.appointmentId === appointmentId
));
if (!queue) {
return `
候诊信息待更新
完成签到后,排队号和预计时间会显示在这里。
`;
}
const updatedAt = typeof queue.updatedAt === "string"
? queue.updatedAt.slice(11, 16)
: "待更新";
return `
神经内科 · 实时候诊
排队号 ${escapeHtml(queue.queueNumber ?? "待更新")}
${escapeHtml(queue.room ?? "诊室待分配")}
${queue.delayed ? `
候诊时间可能延后,请留意叫号更新。
` : ""}
前方人数前方 ${escapeHtml(queue.peopleAhead ?? 0)} 人
预计时间预计等待 ${escapeHtml(queue.estimatedMinutes ?? 0)} 分钟
当前诊室${escapeHtml(queue.room ?? "待分配")}
队列更新时间${escapeHtml(updatedAt)}
`;
};
export const renderRouteView = (location) => `
`;
const pageHeader = (eyebrow, title, description) => `
`;
const careGroupDefinitions = [
{
title: "待办理",
statuses: ["ACTION_REQUIRED", "UPCOMING"],
fallback: "门诊费用待确认",
},
{
title: "进行中",
statuses: ["IN_PROGRESS"],
fallback: "今日就医旅程",
},
{
title: "已完成",
statuses: ["COMPLETED"],
fallback: "身份信息已核验",
},
{
title: "需要人工处理",
statuses: ["MANUAL_REQUIRED"],
fallback: "人工服务随时可用",
},
];
export const renderCareOverview = (state) => {
const items = Array.isArray(state?.patientWorkItems)
? state.patientWorkItems.filter((item) => item && typeof item === "object")
: [];
return `
${pageHeader("就医总览", "我的就医", "按办理状态集中查看门诊、检查和诊后事项。")}
${careGroupDefinitions.map((group) => {
const matches = items.filter((item) => (
group.statuses.includes(item.status)
));
const visible = matches.length > 0
? matches
: [{ title: group.fallback, description: "进入详情查看下一步" }];
return `
${group.title}
${visible.length}
${visible.slice(0, 3).map((item) => `
${escapeHtml(item.title)}
${escapeHtml(item.description ?? "状态和可用操作持续更新")}
`).join("")}
`;
}).join("")}
`;
};
const recordEntries = [
{ id: "lab-reports", label: "检验报告", meta: "血常规 · 3 项异常", filter: "abnormal", icon: "flask-conical", action: "open-report-center", reportFamily: "LAB" },
{ id: "exam-reports", label: "检查报告", meta: "影像与超声报告", filter: "all", icon: "scan-line", action: "open-report-center", reportFamily: "EXAM" },
{ id: "visits", label: "门诊记录", meta: "历次接诊与诊后记录", filter: "all", icon: "notebook-tabs" },
{ id: "medication", label: "处方与用药", meta: "处方、用药提醒", filter: "all", icon: "pill" },
{ id: "allergies", label: "过敏史", meta: "暂未记录药物过敏", filter: "all", icon: "shield-alert" },
{ id: "consents", label: "授权记录", meta: "查看和撤回数据授权", filter: "unread", icon: "file-lock-2" },
];
export const renderRecords = (state) => {
const activeFilter = ["all", "abnormal", "unread"].includes(
state?.recordsFilter,
) ? state.recordsFilter : "all";
const visible = activeFilter === "all"
? recordEntries
: recordEntries.filter((entry) => entry.filter === activeFilter);
return `
${pageHeader("健康档案", "我的健康记录", "报告、门诊记录和授权信息统一归档。")}
${[
["all", "全部"],
["abnormal", "异常"],
["unread", "未解读"],
].map(([id, label]) => `
`).join("")}
${visible.map((entry) => `
<${entry.action ? "button" : "article"} class="compact-row"${entry.action ? ` type="button" data-action="${entry.action}" data-filter="${entry.reportFamily}"` : ""}>
${entry.label}${entry.meta}
${entry.action ? "button" : "article"}>
`).join("")}
`;
};
const profileEntries = [
["就诊人管理", "users-round"],
["授权中心", "shield-check"],
["消息设置", "bell-ring"],
["常用院区", "hospital"],
["医保和支付", "badge-cent"],
["电子票据", "receipt-text"],
["人工客服", "headphones"],
["关于 AI", "sparkles"],
];
export const renderProfile = () => `
${pageHeader("个人中心", "我的", "管理就诊人、隐私授权和常用服务设置。")}
${profileEntries.map(([label, icon]) => `
`).join("")}
`;
const messageCategories = ["报告", "就医", "缴费", "随访", "系统"];
export const renderMessages = (state) => {
const messages = Array.isArray(state?.messages)
? state.messages.filter((message) => message && typeof message === "object")
: [];
return `
${pageHeader("消息中心", "消息", "按服务类型定位到对应就医事项,不会直接执行办理操作。")}
${messageCategories.map((category) => `${category}`).join("")}
${messages.length > 0 ? messages.map((message) => `
`).join("") : `
`}
`;
};
export const renderSystemState = (type, state) => {
const cachedTaskCount = Array.isArray(state?.patientWorkItems)
? state.patientWorkItems.length
: 0;
const definitions = {
INITIAL_LOADING: ["正在加载患者服务台", "正在同步最新就医事项,请稍候。", "loader-circle"],
EMPTY: ["暂无相关内容", "可以返回首页或尝试其他服务。", "inbox"],
OFFLINE: ["网络连接不可用", `已保留 ${cachedTaskCount} 项本地任务,联网后将自动更新。`, "wifi-off"],
DEGRADED: ["部分智能服务暂不可用", "基础就医事项仍可查看,也可以联系人工服务。", "shield-alert"],
QUERY_FAILED: ["查询失败", "请稍后重新查询,旧结果不会用于继续办理。", "search-x"],
COMMAND_FAILED: ["办理失败", "本次操作未完成,请重新查询或联系人工服务。", "circle-x"],
COMMAND_UNKNOWN: ["结果确认中,请勿重复操作", "正在核验医院系统结果,可以稍后查看状态。", "circle-ellipsis"],
};
const [title, description, icon] = definitions[type] ?? definitions.EMPTY;
return `
${title}
${description}
`;
};
export const renderScenarioPanel = (state) => {
const activeScenario = demoScenarios.includes(state?.demoScenario)
? state.demoScenario
: "EMPTY_HOME";
const activeTask = state?.activeAgentTaskProjection;
const commands = Object.values(state?.commandProjections ?? {});
const latestFixture = state?.appliedProjectionFixtureIds?.at?.(-1) ?? "无";
const renderScenarioGroup = (groupId, title, scenarios) => `
${title}
${scenarios.map((scenario) => `
`).join("")}
`;
return `
${renderScenarioGroup(
"reportClosureScenarios",
"报告闭环",
demoScenarioGroups.REPORT_CLOSURE,
)}
${renderScenarioGroup(
"careClosureScenarios",
"就医闭环",
demoScenarioGroups.CARE_CLOSURE,
)}
- 当前 fixture
- ${escapeHtml(latestFixture)}
- Agent task
- ${escapeHtml(activeTask ? `${activeTask.type}/${activeTask.stage}` : "无")}
- PatientWorkItem
- ${escapeHtml(state?.patientWorkItems?.length ?? 0)} 项
- Command
- ${escapeHtml(commands.map((item) => `${item.commandId}/${item.status}`).join(", ") || "无")}
←/→ 切换 · R 重置 · P 显示/隐藏
`;
};
const renderCurrentView = (state) => {
const { reportId, reportView } = state?.overlays ?? {};
if (state?.systemState === "AI_INTERPRETING") {
return renderReportInterpreting();
}
if (state?.systemState) {
return renderSystemState(state.systemState, state);
}
if (state?.overlays?.journeyView === "CHECKIN_CONFIRMATION") {
return `
到院签到
请确认本人已到达诊区
签到需要患者明确确认;本页面不会根据位置自动完成签到。
`;
}
if (state?.overlays?.journeyView === "ROUTE") {
const journey = state.journeyProjections?.find(
(item) => item?.appointmentId === state.overlays.appointmentId,
);
const checkin = state.patientWorkItems?.find(
(item) => (
item?.type === "CHECK_IN"
&& item.appointmentId === state.overlays.appointmentId
),
);
return renderRouteView(
journey?.location ?? checkin?.location ?? journey?.room,
);
}
if (
state?.overlays?.journeyView === "QUEUE"
&& state?.overlays?.appointmentId
) {
return renderQueueStatus(state, state.overlays.appointmentId);
}
if (reportView === "REPORT_DETAIL" && reportId) {
const report = state.reportSourceProjections?.[reportId];
return report?.family === "EXAM"
? renderExaminationReportDetail(report)
: renderReportDetail(report, Boolean(state.overlays.reportExpanded));
}
if (reportView === "ORIGINAL_REPORT" && reportId) {
return renderOriginalReport(state.reportSourceProjections?.[reportId]);
}
if (reportView === "REPORT_UNAVAILABLE" && reportId) {
return renderReportUnavailable({
reason: state?.overlays?.reportUnavailableReason,
reportId,
serviceNotice: state?.overlays?.reportServiceNotice,
});
}
if (reportView === "INTERPRETING" && reportId) {
return renderReportInterpreting();
}
if (
reportView === "INTERPRETED"
&& hasValidReportConsent(state, reportId)
) {
return renderReportInterpretationResult(
selectValidReportResult(state, reportId),
{
feedbackOpen: state?.overlays?.reportFeedbackOpen === true,
feedbackSubmitted: state?.overlays?.reportFeedbackSubmitted === true,
serviceNotice: state?.overlays?.reportServiceNotice,
},
);
}
if (state?.activeTab === "assistant") {
return renderAssistant(state);
}
if (state?.activeTab === "care") {
return renderCareOverview(state);
}
if (state?.activeTab === "records") {
return state?.overlays?.reportView === "REPORT_CENTER"
? renderReportCenter(state)
: renderRecords(state);
}
if (state?.activeTab === "profile") {
return renderProfile(state);
}
if (state?.activeTab === "messages") {
return renderMessages(state);
}
return renderHome(state);
};
let modalOverlayIdentity = null;
let modalOverlayDocument = null;
let consentFocusReturn = null;
let scenarioPanelVisible = true;
let demoViewport = "390";
const hasConsentDialog = (state) => (
typeof state?.overlays?.consentId === "string"
&& state.overlays.consentId.startsWith("consent-ai-")
);
const hasCommandDialog = (state) => Boolean(
state?.overlays?.commandId
&& state?.commandProjections?.[state.overlays.commandId]
?.status === "AWAITING_CONFIRMATION"
);
const hasReportServiceDialog = (state) => (
state?.overlays?.reportView === "REPORT_UNAVAILABLE"
&& state?.overlays?.reportServiceNotice
=== "已为你打开人工服务入口。"
);
const focusModalDialog = (overlayRoot) => {
const dialog = overlayRoot?.querySelector?.('[role="dialog"]');
const firstButton = dialog?.querySelector?.("button");
(firstButton ?? dialog)?.focus?.();
};
const restoreConsentFocus = (root, elements) => {
const matchingTrigger = Array.from(
root?.querySelectorAll?.('[data-action="interpret-report"]') ?? [],
).find((trigger) => (
trigger.dataset?.reportId === consentFocusReturn?.reportId
));
const fallback = elements.appView.querySelector?.(
"button, [href], [tabindex]",
) ?? elements.appView;
(matchingTrigger ?? fallback)?.focus?.();
consentFocusReturn = null;
};
export const renderAppShell = (state = createInitialState()) => {
const root = globalThis.document;
const elements = getShellElements(root);
if (!elements) {
return false;
}
if (root !== modalOverlayDocument) {
modalOverlayIdentity = null;
modalOverlayDocument = root;
}
elements.appHeader.innerHTML = renderHeader(state);
elements.appView.innerHTML = renderCurrentView(state);
elements.bottomNav.innerHTML = navItems.map(
({ id, label, icon }) => `
`,
).join("");
const scenarioPanel = root?.getElementById?.("scenarioPanel");
const phoneShell = root?.querySelector?.(".phone-shell");
if (scenarioPanel) {
scenarioPanel.innerHTML = renderScenarioPanel(state);
scenarioPanel.hidden = !scenarioPanelVisible;
}
if (phoneShell?.dataset) {
phoneShell.dataset.viewport = demoViewport;
}
const overlayRoot = root?.getElementById?.("overlayRoot");
const hasConsentOverlay = Boolean(overlayRoot) && hasConsentDialog(state);
const activeCommand = state?.overlays?.commandId
? state?.commandProjections?.[state.overlays.commandId]
: null;
const hasCommandOverlay = Boolean(overlayRoot) && hasCommandDialog(state);
const hasReportServiceOverlay = Boolean(overlayRoot)
&& hasReportServiceDialog(state);
const nextModalIdentity = hasConsentOverlay
? `consent:${state.overlays.consentId}`
: hasCommandOverlay
? `command:${activeCommand.commandId}:${activeCommand.status}`
: hasReportServiceOverlay
? `report-service:${state.overlays.reportId}`
: null;
const hasModalOverlay = Boolean(nextModalIdentity);
for (const element of [
elements.appHeader,
elements.appView,
elements.bottomNav,
scenarioPanel,
]) {
if (!element) {
continue;
}
element.inert = hasModalOverlay;
element.setAttribute?.(
"aria-hidden",
hasModalOverlay ? "true" : "false",
);
}
if (overlayRoot) {
overlayRoot.innerHTML = hasConsentOverlay
? renderConsentOverlay(state)
: hasCommandOverlay
? renderCommandOverlay(activeCommand)
: hasReportServiceOverlay
? renderReportServiceOverlay(state)
: "";
if (nextModalIdentity && nextModalIdentity !== modalOverlayIdentity) {
focusModalDialog(overlayRoot);
} else if (!nextModalIdentity && modalOverlayIdentity) {
restoreConsentFocus(root, elements);
}
}
modalOverlayIdentity = nextModalIdentity;
renderIcons();
return true;
};
let demoState = loadScenario(createInitialState(), "EMPTY_HOME");
const boundDocuments = new WeakSet();
let reportProjectionGeneration = 0;
let activeReportProjectionTimer = null;
let registrationSelectionFixtureSequence = 0;
let commandConfirmationInFlight = null;
let lastCommandConfirmationRequest = null;
let commandRejectionInFlight = null;
let lastCommandRejectionRequest = null;
export const getDemoState = () => demoState;
export const getLastCommandConfirmationRequest = () => (
lastCommandConfirmationRequest
);
export const getLastCommandRejectionRequest = () => (
lastCommandRejectionRequest
);
const simulateCandidateSelectionBackendFixture = (state, interaction) => {
registrationSelectionFixtureSequence += 1;
const task = state?.activeAgentTaskProjection;
const result = task?.activeResultRef
? state?.resultSnapshotProjections?.[task.activeResultRef]
: null;
const now = Date.parse(state?.demoNow);
const expiresAt = Date.parse(result?.expiresAt);
const versionMatched = interaction?.resultRef === result?.resultRef
&& interaction?.resultVersion === result?.resultVersion;
const notExpired = Number.isFinite(now)
&& Number.isFinite(expiresAt)
&& expiresAt > now;
const realtimeAvailable = Array.isArray(result?.items)
&& result.items.some(
(item) => item?.candidateId === interaction?.candidateId,
);
const errorCode = !notExpired
? "RESULT_EXPIRED"
: !versionMatched
? "RESULT_VERSION_CONFLICT"
: !realtimeAvailable
? "SLOT_UNAVAILABLE"
: null;
const fixtureId = `selection-response-${registrationSelectionFixtureSequence}`;
if (errorCode) {
return {
id: fixtureId,
errorCode,
validation: {
tenantMatched: true,
patientMatched: true,
sessionMatched: true,
versionMatched,
notExpired,
realtimeAvailable,
l1LockSucceeded: false,
},
};
}
const lockExpiresAt = new Date(
Math.min(now + 5 * 60 * 1000, expiresAt - 1),
).toISOString();
return {
id: fixtureId,
outcome: "SUCCESS",
...interaction,
validation: {
tenantMatched: true,
patientMatched: true,
sessionMatched: true,
versionMatched,
notExpired,
realtimeAvailable,
l1LockSucceeded: true,
},
lock: {
lockId: `appointment-lock-${registrationSelectionFixtureSequence}`,
expiresAt: lockExpiresAt,
},
};
};
const submitAssistantText = (state, text) => {
const normalizedText = typeof text === "string" ? text.trim() : text;
const interaction = buildCandidateSelectionInteraction(
state,
normalizedText,
);
if (
!interaction
&& !(
normalizedText === "选第一个"
&& state?.activeAgentTaskProjection?.type === "REGISTRATION"
)
) {
return submitInteraction(state, normalizedText);
}
const responseFixture = simulateCandidateSelectionBackendFixture(
state,
interaction,
);
const selectedState = applyCandidateSelectionResponseFixture(
state,
responseFixture,
);
return applyRegistrationCommandBackendFixture(selectedState);
};
const applyRegistrationCommandBackendFixture = (state) => {
const task = state?.activeAgentTaskProjection;
const result = state?.resultSnapshotProjections?.[task?.activeResultRef];
const selected = result?.items?.find(
(item) => item?.candidateId === task?.selectedCandidateId,
);
if (task?.stage !== "AWAITING_REGISTRATION_CONFIRMATION" || !selected) {
return state;
}
const commands = Object.values(state?.commandProjections ?? {});
const nonTerminalStatuses = new Set([
"PREPARED",
"AWAITING_CONFIRMATION",
"EXECUTING",
"UNKNOWN",
]);
const existing = commands.find((command) => (
command?.details?.candidateId === selected.candidateId
&& nonTerminalStatuses.has(command.status)
));
if (existing) {
return {
...state,
overlays: { ...state.overlays, commandId: existing.commandId },
};
}
const nextSequence = commands.reduce((highest, command) => {
const sequence = Number.parseInt(
command?.commandId?.match(/^command-register-(\d+)$/)?.[1],
10,
);
return Number.isInteger(sequence) ? Math.max(highest, sequence) : highest;
}, 0) + 1;
const commandId = `command-register-${String(nextSequence).padStart(3, "0")}`;
return applyCommandProjectionFixture(state, {
id: `command-awaiting-${commandId}`,
commandId,
version: 1,
riskLevel: "L2",
status: "AWAITING_CONFIRMATION",
details: {
operationName: "确认挂号",
patientName: "张先生",
departmentName: selected.departmentName,
doctorName: selected.doctorName,
doctorTitle: selected.doctorTitle,
visitAt: `${selected.visitDate} ${selected.visitTime}`,
fee: selected.fee,
expiresAt: task.lock?.expiresAt,
countdown: "剩余 5 分钟",
candidateId: selected.candidateId,
},
});
};
const reduceDemoAction = (
state,
action,
reportId,
candidateId,
assistantText,
appointmentId,
tabId,
filter,
messageId,
scenarioName,
viewport,
) => {
switch (action) {
case "view-report":
return openReportDetails(state, reportId);
case "view-original-report":
return state.reportSourceProjections?.[reportId] ? {
...state,
overlays: {
...state.overlays,
reportId,
reportView: "ORIGINAL_REPORT",
},
} : state;
case "open-report-center": {
const reportCenterState = closeReportView(state);
return {
...reportCenterState,
activeTab: "records",
reportFilters: {
...reportCenterState.reportFilters,
family: ["LAB", "EXAM"].includes(filter) ? filter : "ALL",
},
overlays: {
...reportCenterState.overlays,
reportView: "REPORT_CENTER",
},
};
}
case "set-report-family-filter":
return {
...state,
activeTab: "records",
reportFilters: {
...state.reportFilters,
family: ["ALL", "LAB", "EXAM"].includes(filter) ? filter : "ALL",
},
};
case "set-report-status-filter":
return {
...state,
activeTab: "records",
reportFilters: {
...state.reportFilters,
status: ["ALL", "UNREAD", "READ"].includes(filter) ? filter : "ALL",
},
};
case "toggle-report-metrics":
return {
...state,
overlays: {
...state.overlays,
reportExpanded: !state.overlays?.reportExpanded,
},
};
case "interpret-report":
return requestReportInterpretation(state, reportId);
case "grant-report-consent":
return grantConsent(state, state?.overlays?.consentId);
case "reject-report-consent":
return rejectConsent(state, state?.overlays?.consentId);
case "open-report-feedback":
return (
reportId === state?.overlays?.reportId
&& state?.overlays?.reportView === "INTERPRETED"
&& hasValidReportConsent(state, reportId)
&& selectValidReportResult(state, reportId)
) ? {
...state,
overlays: {
...state.overlays,
reportFeedbackOpen: true,
reportFeedbackSubmitted: false,
reportServiceNotice: null,
},
} : state;
case "submit-report-feedback": {
if (
reportId !== state?.overlays?.reportId
|| state?.overlays?.reportView !== "INTERPRETED"
) {
return state;
}
const feedbackState = submitReportFeedback(state, {
reportId,
category: filter,
});
return feedbackState === state ? state : {
...feedbackState,
overlays: {
...feedbackState.overlays,
reportFeedbackOpen: false,
reportFeedbackSubmitted: true,
reportServiceNotice: null,
},
};
}
case "consult-ordering-doctor":
case "online-consultation":
case "contact-human-service": {
if (
action === "contact-human-service"
&& reportId === state?.overlays?.reportId
&& state?.overlays?.reportView === "REPORT_UNAVAILABLE"
&& state?.reportSourceProjections?.[reportId]
) {
return {
...state,
overlays: {
...state.overlays,
reportServiceNotice: "已为你打开人工服务入口。",
},
};
}
if (
reportId !== state?.overlays?.reportId
|| state?.overlays?.reportView !== "INTERPRETED"
|| !hasValidReportConsent(state, reportId)
|| !selectValidReportResult(state, reportId)
) {
return state;
}
const notices = {
"consult-ordering-doctor": "已为你打开开单医生咨询入口。",
"online-consultation": "已为你打开在线咨询入口。",
"contact-human-service": "已为你打开人工服务入口。",
};
return {
...state,
overlays: {
...state.overlays,
reportFeedbackOpen: false,
reportServiceNotice: notices[action],
},
};
}
case "close-report-service":
return hasReportServiceDialog(state) ? {
...state,
overlays: {
...state.overlays,
reportServiceNotice: null,
},
} : state;
case "revoke-report-consent": {
const activeReportId = state?.overlays?.reportId;
const activeConsentId = typeof activeReportId === "string"
? `consent-ai-${activeReportId}`
: Object.keys(state?.consents ?? {}).find(
(consentId) => state.consents[consentId]?.status === "GRANTED",
);
return revokeConsent(state, activeConsentId);
}
case "go-home": {
const homeState = closeReportView(state);
return {
...homeState,
activeTab: "home",
overlays: {
...homeState.overlays,
appointmentId: null,
journeyView: null,
},
};
}
case "open-checkin-confirmation":
return {
...state,
overlays: {
...state.overlays,
appointmentId,
journeyView: "CHECKIN_CONFIRMATION",
},
};
case "open-queue":
return {
...state,
overlays: {
...state.overlays,
appointmentId,
journeyView: "QUEUE",
},
};
case "open-route":
return {
...state,
overlays: {
...state.overlays,
appointmentId,
journeyView: "ROUTE",
},
};
case "switch-patient": {
commandConfirmationInFlight = null;
commandRejectionInFlight = null;
invalidateReportProjectionTimer();
const nextPatientId = state.activePatientId === "patient-li-na"
? "patient-zhang-san"
: "patient-li-na";
return {
...createInitialState({ activePatientId: nextPatientId }),
demoScenario: "EMPTY_HOME",
};
}
case "submit-checkin": {
const checkedIn = applyServerProjectionFixture(state, {
id: `checkin-completed-${appointmentId}`,
type: "CHECKIN_COMPLETED",
occurredAt: state.demoNow,
payload: {
appointmentId,
queueNumber: "A023",
room: "3F-08诊室",
},
});
return {
...checkedIn,
overlays: {
...checkedIn.overlays,
appointmentId,
journeyView: "QUEUE",
},
};
}
case "open-assistant":
case "return-current-task":
return {
...closeReportView(state),
activeTab: "assistant",
};
case "navigate-tab": {
const tabState = closeReportView(state);
return {
...tabState,
activeTab: navItems.some((item) => item.id === tabId) ? tabId : "home",
systemState: null,
overlays: {
...tabState.overlays,
appointmentId: null,
journeyView: null,
},
};
}
case "open-messages":
return {
...closeReportView(state),
activeTab: "messages",
systemState: null,
};
case "set-record-filter":
return {
...state,
activeTab: "records",
recordsFilter: ["all", "abnormal", "unread"].includes(filter)
? filter
: "all",
};
case "open-message": {
const message = (Array.isArray(state.messages) ? state.messages : [])
.find((item) => item?.id === messageId);
if (message?.type === "REPORT") {
const reportId = typeof message.reportId === "string"
? message.reportId
: null;
return reportId ? openReportDetails(state, reportId) : state;
}
if (message?.type === "JOURNEY") {
const journey = state.journeyProjections?.[0];
return journey ? {
...state,
activeTab: journey.stage === "QUEUE_WAITING" ? state.activeTab : "care",
overlays: {
...state.overlays,
appointmentId: journey.appointmentId,
journeyView: journey.stage === "QUEUE_WAITING" ? "QUEUE" : null,
},
} : state;
}
return state;
}
case "load-scenario":
case "reset-scenario": {
commandConfirmationInFlight = null;
commandRejectionInFlight = null;
invalidateReportProjectionTimer();
return loadScenario(
state,
action === "reset-scenario"
? state.demoScenario ?? "EMPTY_HOME"
: scenarioName,
);
}
case "previous-scenario":
case "next-scenario": {
const currentIndex = Math.max(
0,
demoScenarios.indexOf(state.demoScenario),
);
const delta = action === "previous-scenario" ? -1 : 1;
const nextIndex = (
currentIndex + delta + demoScenarios.length
) % demoScenarios.length;
commandConfirmationInFlight = null;
commandRejectionInFlight = null;
invalidateReportProjectionTimer();
return loadScenario(state, demoScenarios[nextIndex]);
}
case "toggle-scenario-panel":
scenarioPanelVisible = !scenarioPanelVisible;
return state;
case "set-demo-viewport":
demoViewport = viewport === "430" ? "430" : "390";
return state;
case "submit-registration-demo":
return submitAssistantText(
state,
"我想挂明天神经内科李明主任的号",
);
case "submit-assistant-text":
return submitAssistantText(state, assistantText);
case "select-candidate": {
const interaction = buildCandidateSelectionInteraction(
state,
candidateId,
);
return applyRegistrationCommandBackendFixture(
applyCandidateSelectionResponseFixture(
state,
simulateCandidateSelectionBackendFixture(state, interaction),
),
);
}
case "close-command":
return {
...state,
activeTab: "home",
overlays: { ...state.overlays, commandId: null },
};
case "requery-registration":
commandConfirmationInFlight = null;
return submitInteraction({
...state,
overlays: { ...state.overlays, commandId: null },
}, "我想挂明天神经内科李明主任的号");
default:
return state;
}
};
const selectInterpretingReportId = (state) => {
const reportId = state?.overlays?.reportId;
const hasInterpretingItem = state?.patientWorkItems?.some(
(item) => item?.type === "REPORT_REVIEW"
&& item.reportId === reportId
&& item.stage === "INTERPRETING",
);
return typeof reportId === "string"
&& state?.overlays?.reportView === "INTERPRETING"
&& evaluateReportInterpretationGate(state, reportId).allowed
&& state?.reportSourceProjections?.[reportId]?.interpretationStatus
=== "GENERATING"
&& hasValidReportConsent(state, reportId)
&& hasInterpretingItem
? reportId
: null;
};
const invalidateReportProjectionTimer = () => {
activeReportProjectionTimer = null;
reportProjectionGeneration += 1;
};
const ensureReportInterpretationProjection = () => {
if (demoState?.demoScenario === "REPORT_GENERATING") {
invalidateReportProjectionTimer();
return;
}
const reportId = selectInterpretingReportId(demoState);
if (!reportId) {
invalidateReportProjectionTimer();
return;
}
if (
activeReportProjectionTimer?.reportId === reportId
&& activeReportProjectionTimer.generation === reportProjectionGeneration
) {
return;
}
if (typeof globalThis.setTimeout !== "function") {
return;
}
const scheduledGeneration = reportProjectionGeneration;
const consentId = `consent-ai-${reportId}`;
const timerToken = {
generation: scheduledGeneration,
reportId,
patientId: demoState?.activePatientId,
stateContext: demoState,
consentContext: demoState?.consents?.[consentId],
bindingContext: demoState?.consentScopeBindings,
};
activeReportProjectionTimer = timerToken;
globalThis.setTimeout(() => {
if (activeReportProjectionTimer !== timerToken) {
return;
}
activeReportProjectionTimer = null;
const isCurrentInterpretation = (
reportProjectionGeneration === scheduledGeneration
&& selectInterpretingReportId(demoState) === reportId
&& demoState?.activePatientId === timerToken.patientId
&& demoState === timerToken.stateContext
&& demoState?.consents?.[consentId]
=== timerToken.consentContext
&& demoState?.consentScopeBindings === timerToken.bindingContext
);
if (!isCurrentInterpretation) {
return;
}
demoState = applyReportInterpretationProjectionFixture(
demoState,
reportId,
);
renderAppShell(demoState);
}, 120);
};
const beginCommandConfirmation = () => {
const commandId = demoState?.overlays?.commandId;
if (commandConfirmationInFlight || commandRejectionInFlight || !commandId) {
return;
}
lastCommandConfirmationRequest = buildCommandConfirmationRequest(
demoState,
commandId,
{
channel: "CARD",
idempotencyKey: `confirm-${commandId.replace(/^command-/, "")}`,
},
);
const token = { commandId, request: lastCommandConfirmationRequest };
commandConfirmationInFlight = token;
if (typeof globalThis.setTimeout !== "function") {
return;
}
globalThis.setTimeout(() => {
if (
commandConfirmationInFlight !== token
|| demoState?.commandProjections?.[commandId]?.status
!== "AWAITING_CONFIRMATION"
) {
return;
}
demoState = applyCommandProjectionFixture(demoState, {
id: `command-executing-${commandId}`,
commandId,
version: demoState.commandProjections[commandId].version + 1,
status: "EXECUTING",
});
renderAppShell(demoState);
globalThis.setTimeout(() => {
if (
commandConfirmationInFlight !== token
|| demoState?.commandProjections?.[commandId]?.status !== "EXECUTING"
) {
return;
}
demoState = applyCommandProjectionFixture(demoState, {
id: `command-succeeded-${commandId}`,
commandId,
version: demoState.commandProjections[commandId].version + 1,
status: "SUCCEEDED",
result: {
type: "APPOINTMENT_CREATED",
eventId: `appointment-event-${commandId}`,
appointmentId: `appointment-${commandId}`,
patientId: demoState.activePatientId,
summary: {
visitDate: demoState.commandProjections[commandId]
.details?.visitAt?.split(" ")?.[0],
visitTime: demoState.commandProjections[commandId]
.details?.visitAt?.split(" ")?.[1],
departmentName: demoState.commandProjections[commandId]
.details?.departmentName,
doctorName: demoState.commandProjections[commandId]
.details?.doctorName,
doctorTitle: demoState.commandProjections[commandId]
.details?.doctorTitle,
fee: demoState.commandProjections[commandId].details?.fee,
},
},
});
commandConfirmationInFlight = null;
renderAppShell(demoState);
}, 120);
}, 80);
};
const beginCommandRejection = () => {
const commandId = demoState?.overlays?.commandId;
if (commandRejectionInFlight || commandConfirmationInFlight || !commandId) {
return;
}
lastCommandRejectionRequest = buildCommandRejectionRequest(
demoState,
commandId,
{
reason: "暂不操作",
idempotencyKey: `reject-${commandId.replace(/^command-/, "")}`,
},
);
const token = { commandId, request: lastCommandRejectionRequest };
commandRejectionInFlight = token;
if (typeof globalThis.setTimeout !== "function") {
return;
}
globalThis.setTimeout(() => {
if (
commandRejectionInFlight !== token
|| demoState?.commandProjections?.[commandId]?.status
!== "AWAITING_CONFIRMATION"
) {
return;
}
demoState = applyCommandProjectionFixture(demoState, {
id: `command-rejected-${commandId}`,
commandId,
version: demoState.commandProjections[commandId].version + 1,
status: "REJECTED",
});
commandRejectionInFlight = null;
renderAppShell(demoState);
}, 80);
};
export const initializePatientServiceDesk = (state = demoState) => {
if (state !== demoState) {
invalidateReportProjectionTimer();
commandConfirmationInFlight = null;
commandRejectionInFlight = null;
lastCommandConfirmationRequest = null;
lastCommandRejectionRequest = null;
}
demoState = state;
const root = globalThis.document;
if (root?.addEventListener && !boundDocuments.has(root)) {
root.addEventListener("click", (event) => {
const trigger = event.target?.closest?.("[data-action]");
const overlayRoot = root.getElementById?.("overlayRoot");
const appView = root.getElementById?.("appView");
const scenarioPanel = root.getElementById?.("scenarioPanel");
const appShell = root.querySelector?.(".phone-shell")
?? appView?.closest?.(".phone-shell");
const withinApp = Boolean(
appShell?.contains?.(trigger)
|| appView?.contains?.(trigger)
|| overlayRoot?.contains?.(trigger)
|| scenarioPanel?.contains?.(trigger),
);
if (!trigger || !withinApp) {
return;
}
if (
(
hasConsentDialog(demoState)
|| hasCommandDialog(demoState)
|| hasReportServiceDialog(demoState)
)
&& scenarioPanel?.contains?.(trigger)
) {
return;
}
if (trigger.dataset?.action === "interpret-report") {
consentFocusReturn = {
reportId: trigger.dataset?.reportId,
};
}
if (trigger.dataset?.action === "confirm-command") {
beginCommandConfirmation();
return;
}
if (trigger.dataset?.action === "reject-command") {
beginCommandRejection();
return;
}
demoState = reduceDemoAction(
demoState,
trigger.dataset?.action,
trigger.dataset?.reportId,
trigger.dataset?.candidateId,
root.getElementById?.("assistantComposerInput")?.value,
trigger.dataset?.appointmentId,
trigger.dataset?.tabId,
trigger.dataset?.filter,
trigger.dataset?.messageId,
trigger.dataset?.scenario,
trigger.dataset?.viewport,
);
renderAppShell(demoState);
ensureReportInterpretationProjection();
});
root.addEventListener("keydown", (event) => {
if (
event.key === "Enter"
&& event.target?.id === "assistantComposerInput"
) {
const appView = root.getElementById?.("appView");
if (!appView?.contains?.(event.target)) {
return;
}
event.preventDefault?.();
demoState = reduceDemoAction(
demoState,
"submit-assistant-text",
undefined,
undefined,
event.target.value,
);
renderAppShell(demoState);
ensureReportInterpretationProjection();
return;
}
const targetTag = event.target?.tagName?.toLowerCase?.();
const isTyping = (
["input", "textarea", "select"].includes(targetTag)
|| event.target?.isContentEditable
);
const isPresentationShortcut = (
!isTyping
&& ["ArrowLeft", "ArrowRight", "r", "R", "p", "P"]
.includes(event.key)
);
const consentDialogOpen = hasConsentDialog(demoState);
const commandDialogOpen = hasCommandDialog(demoState);
const reportServiceDialogOpen = hasReportServiceDialog(demoState);
if (
isPresentationShortcut
&& (
consentDialogOpen
|| commandDialogOpen
|| reportServiceDialogOpen
)
) {
event.preventDefault?.();
return;
}
if (isPresentationShortcut) {
event.preventDefault?.();
if (event.key === "p" || event.key === "P") {
scenarioPanelVisible = !scenarioPanelVisible;
renderAppShell(demoState);
return;
}
const currentIndex = Math.max(
0,
demoScenarios.indexOf(demoState.demoScenario),
);
const nextScenario = event.key === "r" || event.key === "R"
? demoState.demoScenario ?? "EMPTY_HOME"
: demoScenarios[(
currentIndex
+ (event.key === "ArrowLeft" ? -1 : 1)
+ demoScenarios.length
) % demoScenarios.length];
initializePatientServiceDesk(loadScenario(demoState, nextScenario));
return;
}
if (
!consentDialogOpen
&& !commandDialogOpen
&& !reportServiceDialogOpen
) {
return;
}
if (event.key === "Escape" && reportServiceDialogOpen) {
event.preventDefault?.();
demoState = reduceDemoAction(
demoState,
"close-report-service",
);
renderAppShell(demoState);
return;
}
if (event.key === "Escape" && consentDialogOpen) {
event.preventDefault?.();
demoState = rejectConsent(
demoState,
demoState.overlays.consentId,
);
renderAppShell(demoState);
ensureReportInterpretationProjection();
return;
}
if (event.key !== "Tab") {
return;
}
const overlayRoot = root.getElementById?.("overlayRoot");
const focusables = Array.from(
overlayRoot?.querySelectorAll?.(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
) ?? [],
);
if (focusables.length === 0) {
event.preventDefault?.();
overlayRoot?.querySelector?.('[role="dialog"]')?.focus?.();
return;
}
const activeIndex = focusables.indexOf(root.activeElement);
const nextIndex = event.shiftKey
? (activeIndex <= 0 ? focusables.length - 1 : activeIndex - 1)
: (activeIndex === focusables.length - 1 ? 0 : activeIndex + 1);
event.preventDefault?.();
focusables[nextIndex]?.focus?.();
});
boundDocuments.add(root);
}
const rendered = renderAppShell(demoState);
ensureReportInterpretationProjection();
return rendered;
};
if (globalThis.document) {
if (globalThis.document.readyState === "loading") {
globalThis.document.addEventListener(
"DOMContentLoaded",
() => initializePatientServiceDesk(),
{ once: true },
);
} else {
initializePatientServiceDesk();
}
}