| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649 |
- 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 `
- <div class="hospital-context">
- <span class="hospital-logo" aria-hidden="true">
- <i data-lucide="hospital"></i>
- </span>
- <div class="hospital-context__copy">
- <p class="app-kicker">空海医院</p>
- <p class="campus-label">当前院区 · 空海医院本院</p>
- </div>
- <button class="icon-button" type="button" aria-label="${messageLabel}" data-action="open-messages">
- <i data-lucide="bell"></i>
- ${unreadCount > 0 ? `<span class="unread-badge" aria-hidden="true">${unreadCount}</span>` : ""}
- </button>
- <button class="icon-button" type="button" aria-label="隐私设置">
- <i data-lucide="shield-check"></i>
- </button>
- </div>
- <button class="patient-switcher" type="button" aria-label="切换当前就诊人" data-action="switch-patient">
- <span>当前就诊人</span>
- <strong>${patientDisplayName(state)}</strong>
- <i data-lucide="chevron-down"></i>
- </button>
- `;
- };
- 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 `
- <article class="journey-card">
- <div class="journey-card__heading">
- <div>
- <strong>${escapeHtml(journey.title)}</strong>
- <p>${escapeHtml(journey.location ?? journey.room ?? "院内行程持续更新")}</p>
- </div>
- ${journey.stage === "QUEUE_WAITING" ? `
- <button class="text-button" type="button" data-action="open-queue" data-appointment-id="${escapeHtml(journey.appointmentId)}">
- 查看候诊
- </button>
- ` : ""}
- </div>
- <ol class="journey-steps">
- ${visibleStages.map((node) => `
- <li class="${node.stage === journey.stage ? "is-current" : ""}">
- <time>${node.time}</time>
- <span>${node.label}</span>
- </li>
- `).join("")}
- </ol>
- </article>
- `;
- };
- 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 `
- <section class="ai-welcome" aria-labelledby="welcomeTitle">
- <div>
- <p class="section-eyebrow">AI 就医助手</p>
- <h1 id="welcomeTitle">${patientDisplayName(state)},上午好</h1>
- <p>${activeTaskCount > 0 ? `有 ${activeTaskCount} 项就医事项需要关注` : "今天暂无待处理的就医事项"}</p>
- </div>
- <img class="doctor-avatar" src="./assets/doctor.png" alt="空海医院 AI 医生助手" />
- <button class="assistant-input" type="button" data-action="open-assistant">
- <i data-lucide="sparkles"></i>
- <span>问病情、查报告、找医生、办就医服务</span>
- <i data-lucide="arrow-up-right"></i>
- </button>
- </section>
- <section class="home-section" aria-labelledby="tasksTitle">
- <div class="section-heading">
- <div>
- <p class="section-eyebrow">就医任务</p>
- <h2 id="tasksTitle">现在要做</h2>
- </div>
- </div>
- <div class="task-list">
- ${tasks.length === 0 ? `
- <div class="empty-card">
- <i data-lucide="circle-check-big"></i>
- <div>
- <strong>今天没有待处理事项</strong>
- <p>新的报告、缴费或预约提醒会显示在这里。</p>
- </div>
- </div>
- ` : 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 `
- <article class="task-card task-card--${status.className}">
- <div class="task-card__status">
- <i data-lucide="${status.icon}"></i>
- <span>${status.label}</span>
- </div>
- <h3>${escapeHtml(task.title)}</h3>
- <p><strong>发生了什么:</strong>${escapeHtml(task.description)}</p>
- <p><strong>为什么关注:</strong>${isCompleted ? "该事项已处理完成。" : "及时处理有助于顺利推进后续就医安排。"}</p>
- <div class="task-card__recommendation">
- <span>建议操作</span>
- <strong>${recommendation}</strong>
- </div>
- ${canAct ? `
- <div class="task-card__actions">
- <button class="button ${isPrimaryTask ? "button--primary" : "button--secondary"}" type="button"${renderActionAttributes(task.primaryAction, task)}>
- ${primaryActionLabel}
- </button>
- ${secondaryActionLabel ? `
- <button class="button button--secondary" type="button"${renderActionAttributes(task.secondaryAction, task)}>
- ${secondaryActionLabel}
- </button>
- ` : ""}
- </div>
- ` : ""}
- </article>
- `;
- }).join("")}
- </div>
- </section>
- <section class="home-section" aria-labelledby="journeyTitle">
- <div class="section-heading">
- <div>
- <p class="section-eyebrow">院内进度</p>
- <h2 id="journeyTitle">今日旅程</h2>
- </div>
- </div>
- ${journeys.length > 0 ? journeys.map(renderJourneyProjection).join("") : `
- <div class="journey-empty">
- <i data-lucide="route"></i>
- <div>
- <strong>今天暂无院内行程</strong>
- <p>挂号或检查安排确认后,将展示前后步骤和当前节点。</p>
- </div>
- </div>
- `}
- </section>
- <section class="home-section" aria-labelledby="servicesTitle">
- <div class="section-heading">
- <div>
- <p class="section-eyebrow">快捷入口</p>
- <h2 id="servicesTitle">常用服务</h2>
- </div>
- </div>
- <div class="service-grid">
- ${homeServices.map(({ label, icon }) => `
- <button class="service-entry" type="button">
- <span><i data-lucide="${icon}"></i></span>
- <strong>${label}</strong>
- </button>
- `).join("")}
- </div>
- </section>
- <section class="home-section care-section" aria-labelledby="careTitle">
- <div class="section-heading">
- <div>
- <p class="section-eyebrow">健康提醒</p>
- <h2 id="careTitle">主动关怀</h2>
- </div>
- </div>
- <div class="care-empty">
- <i data-lucide="heart-handshake"></i>
- <div>
- <strong>暂无新的主动关怀提醒</strong>
- <p>用药、复诊与检查准备提醒会在确认后展示。</p>
- </div>
- </div>
- </section>
- `;
- };
- 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 }) => `
- <article class="assistant-message assistant-message--${role}">
- <strong>${role === "user" ? "你" : "AI 助手"}</strong>
- <p>${escapeHtml(text)}</p>
- </article>
- `).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 `
- <section class="assistant-view" aria-labelledby="assistantTitle">
- <header class="assistant-view__header">
- <button class="text-button" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div>
- <p class="section-eyebrow">自然语言就医服务</p>
- <h1 id="assistantTitle">AI 助手</h1>
- </div>
- ${hasTask ? `
- <button class="text-button" type="button" data-action="return-current-task">返回当前任务</button>
- ` : ""}
- </header>
- <section class="assistant-scenarios" aria-labelledby="scenarioTitle">
- <h2 id="scenarioTitle">场景建议</h2>
- <button type="button" data-action="open-report-center" data-filter="ALL">帮我看报告</button>
- <button type="button" data-action="submit-registration-demo">我想挂明天神经内科李明主任的号</button>
- <button type="button">智能导诊</button>
- <button type="button">人工服务</button>
- </section>
- <div class="assistant-thread" aria-live="polite">
- ${renderAssistantMessages(state?.messages)}
- ${task?.stage === "REQUERY_REQUIRED" ? `
- <aside class="assistant-recovery">
- <strong>REQUERY_REQUIRED</strong>
- <p>旧候选已失效,请重新查询后再选择。</p>
- <button class="button button--secondary" type="button" data-action="submit-registration-demo">重新查询</button>
- </aside>
- ` : ""}
- ${result ? `
- <section class="assistant-result" aria-labelledby="slotResultTitle">
- <h2 id="slotResultTitle">可选号源</h2>
- <p class="assistant-answer">${escapeHtml(result.text)}</p>
- ${candidates.length > 0 ? `
- <div class="slot-presentation">
- ${candidates.map((item) => `
- <button class="slot-candidate" type="button" data-action="select-candidate" data-candidate-id="${item.candidateId}">
- <strong>${escapeHtml(item.visitDate)} ${escapeHtml(item.timePeriod)} ${escapeHtml(item.visitTime)}</strong>
- <span>${escapeHtml(item.departmentName)} · ${escapeHtml(item.doctorName)} ${escapeHtml(item.doctorTitle)}</span>
- <span>余号 ${escapeHtml(item.remainingSlots)} · 挂号费 ${escapeHtml(item.fee)}</span>
- </button>
- `).join("")}
- </div>
- ` : ""}
- </section>
- ` : ""}
- ${task?.stage === "AWAITING_REGISTRATION_CONFIRMATION" ? `
- <aside class="assistant-lock-success">
- <strong>锁号成功</strong>
- <p>号源已临时锁定,已收到服务端二次确认投影。</p>
- </aside>
- ` : ""}
- </div>
- <div class="assistant-composer">
- <label class="sr-only" for="assistantComposerInput">输入就医需求</label>
- <input id="assistantComposerInput" type="text" placeholder="描述你想办理的就医服务" />
- <button type="button" data-action="submit-assistant-text" aria-label="发送">
- <i data-lucide="arrow-up"></i>
- </button>
- </div>
- </section>
- `;
- };
- 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
- ? `<small>参考范围 ${escapeHtml(metric.referenceRange)}${unit}</small>`
- : "<small>参考范围未提供</small>";
- const flag = metricFlagLabels[metric?.sourceFlag] ?? "结果待确认";
- return `<li class="report-metric report-metric--${escapeHtml(String(metric?.sourceFlag ?? "unknown").toLowerCase())}">
- <span>${name}</span><strong>${value}${unit}</strong>${range}<em>${flag}</em>
- </li>`;
- };
- 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 `
- <section class="module-page report-center" aria-labelledby="reportCenterTitle">
- ${pageHeader("健康档案", "报告中心", "医院原始报告和 AI 辅助解读统一查看。")}
- <p class="report-center__patient">当前就诊人 ${escapeHtml(patientDisplayName(state))}</p>
- <div class="report-filter-group" role="group" aria-label="报告类型">
- ${[["ALL", "全部"], ["LAB", "检验报告"], ["EXAM", "检查报告"]].map(([id, label]) => `
- <button type="button" data-action="set-report-family-filter" data-filter="${id}" aria-pressed="${filters.family === id}">${label}</button>
- `).join("")}
- </div>
- <div class="report-filter-group report-filter-group--status" role="group" aria-label="阅读状态">
- ${[["ALL", "全部"], ["UNREAD", "未读"], ["READ", "已读"]].map(([id, label]) => `
- <button type="button" data-action="set-report-status-filter" data-filter="${id}" aria-pressed="${filters.status === id}">${label}</button>
- `).join("")}
- </div>
- <div class="report-center__list">
- ${reports.length ? reports.map((report) => {
- const abnormalCount = abnormalMetricsOf(report).length;
- return `
- <button class="report-list-card" type="button" data-action="view-report" data-report-id="${escapeHtml(report.reportId)}">
- <span class="report-list-card__top"><b>${escapeHtml(reportFamilyLabels[report.family] ?? "医院报告")}</b><em>${report.readStatus === "READ" ? "已读" : "未读"}</em></span>
- <strong>${escapeHtml(report.title || "未命名报告")}</strong>
- <span>${escapeHtml(report.hospitalName || "医院信息待确认")} · ${escapeHtml(report.departmentName || "科室待确认")}</span>
- <time>${escapeHtml(report.issuedAt || "出具时间待确认")}</time>
- <span class="report-list-card__status">${escapeHtml(reportSourceStatusLabels[report.sourceStatus] ?? "报告状态待确认")}${report.family === "LAB" ? ` · ${abnormalCount ? `${abnormalCount} 项指标异常` : "未标记异常指标"}` : ""}</span>
- <span>${report.family === "EXAM"
- ? "该类型暂未开放AI辅助解读"
- : escapeHtml(reportInterpretationLabels[report.interpretationStatus] ?? "AI 辅助解读状态待确认")}</span>
- </button>`;
- }).join("") : `<div class="report-empty"><i data-lucide="file-search"></i><strong>暂无符合筛选条件的报告</strong><p>可以调整报告类型或阅读状态。</p></div>`}
- </div>
- </section>`;
- };
- 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 `
- <section class="report-view" aria-labelledby="reportTitle">
- <button class="text-button report-view__back" type="button" data-action="open-report-center">
- <i data-lucide="arrow-left"></i>返回报告中心
- </button>
- <div class="report-hero">
- <p class="section-eyebrow">医院原始报告</p>
- <h1 id="reportTitle">${escapeHtml(report?.title || "未命名报告")}</h1>
- <p>${escapeHtml(report?.hospitalName || "医院信息待确认")} · ${escapeHtml(report?.departmentName || "科室待确认")}</p>
- <p>出具时间:${escapeHtml(report?.issuedAt || "待确认")}</p>
- <span class="report-audit"><i data-lucide="badge-check"></i>${escapeHtml(reportSourceStatusLabels[report?.sourceStatus] ?? "报告状态待确认")}</span>
- </div>
- <div class="report-notice">
- <i data-lucide="shield-alert"></i>
- <p><strong>AI 辅助解读仅供参考</strong>,报告结果需结合症状、病史并由医生判断。</p>
- </div>
- <section class="report-card" aria-labelledby="abnormalTitle">
- <div class="section-heading">
- <div>
- <p class="section-eyebrow">指标概览</p>
- <h2 id="abnormalTitle">${abnormalMetrics.length ? `${abnormalMetrics.length} 项异常指标` : "未标记异常指标"}</h2>
- </div>
- </div>
- <ul class="indicator-list">
- ${visibleMetrics.length ? visibleMetrics.map(renderMetric).join("") : "<li class=\"report-metric\"><span>暂无指标数据</span></li>"}
- </ul>
- ${hiddenMetrics.length ? `
- <button class="button button--secondary" type="button" data-action="toggle-report-metrics">
- ${expanded ? "收起全部指标" : "查看全部指标"}
- </button>
- ` : ""}
- </section>
- <div class="report-links" aria-label="报告相关入口">
- <button type="button" data-action="view-original-report" data-report-id="${escapeHtml(reportId)}"><i data-lucide="file-text"></i>查看原始报告</button>
- </div>
- <button class="button button--primary report-primary" type="button" data-action="interpret-report" data-report-id="${escapeHtml(reportId)}">
- <i data-lucide="sparkles"></i>AI 辅助解读
- </button>
- </section>
- `;
- };
- 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 `
- <section class="report-view examination-report" aria-labelledby="examinationReportTitle">
- <button class="text-button report-view__back" type="button" data-action="open-report-center">
- <i data-lucide="arrow-left"></i>返回报告中心
- </button>
- <div class="report-hero">
- <p class="section-eyebrow">医院检查报告</p>
- <h1 id="examinationReportTitle">${safeReportText(report?.title, "检查名称待确认")}</h1>
- <p>检查部位:${safeReportText(report?.bodyPart, "待确认")}</p>
- <p>${safeReportText(report?.hospitalName, "医院信息待确认")} · ${safeReportText(report?.departmentName, "科室待确认")}</p>
- <p>出具时间:${safeReportText(report?.issuedAt, "待确认")}</p>
- </div>
- <section class="report-card" aria-labelledby="examinationFindingsTitle">
- <p class="section-eyebrow">检查所见</p>
- <h2 id="examinationFindingsTitle">检查所见</h2>
- <p>${safeReportText(report?.findings, "检查所见待确认")}</p>
- </section>
- <section class="report-card" aria-labelledby="examinationConclusionTitle">
- <p class="section-eyebrow">报告结论</p>
- <h2 id="examinationConclusionTitle">报告结论</h2>
- <p>${safeReportText(report?.conclusion, "报告结论待确认")}</p>
- </section>
- <section class="report-card examination-report__audit" aria-label="报告审核信息">
- <p><strong>报告医生:</strong>${safeReportText(report?.reportingDoctor, "报告医生待确认")}</p>
- <p><strong>审核状态:</strong>${safeReportText(
- report?.reviewStatus,
- reportSourceStatusLabels[report?.sourceStatus] ?? "审核状态待确认",
- )}</p>
- </section>
- <div class="report-links" aria-label="检查报告原件入口">
- <button type="button" data-action="view-original-report" data-report-id="${escapeHtml(reportId)}">
- <i data-lucide="images"></i>查看图像或PDF
- </button>
- </div>
- <div class="report-notice">
- <i data-lucide="info"></i>
- <p>该类型暂未开放AI辅助解读</p>
- </div>
- </section>
- `;
- };
- export const renderOriginalReport = (report = {}) => {
- if (report?.family === "EXAM") {
- return `
- <section class="report-view original-report examination-original" aria-labelledby="originalExaminationTitle">
- <button class="text-button report-view__back" type="button" data-action="view-report" data-report-id="${escapeHtml(report?.reportId)}">
- <i data-lucide="arrow-left"></i>返回报告详情
- </button>
- <div class="report-hero">
- <p class="section-eyebrow">医院检查报告原件</p>
- <h1 id="originalExaminationTitle">${safeReportText(report?.title, "检查名称待确认")}</h1>
- <p>${safeReportText(report?.originalFormat, "图像或PDF")}</p>
- </div>
- <div class="report-notice">
- <i data-lucide="file-check"></i>
- <p>原始图像或PDF由医院报告系统提供,本原型展示统一查看入口。</p>
- </div>
- </section>
- `;
- }
- const metrics = Array.isArray(report?.metrics) ? report.metrics : [];
- return `
- <section class="report-view original-report" aria-labelledby="originalReportTitle">
- <button class="text-button report-view__back" type="button" data-action="view-report" data-report-id="${escapeHtml(report?.reportId)}">
- <i data-lucide="arrow-left"></i>返回报告详情
- </button>
- <div class="report-hero">
- <p class="section-eyebrow">医院原始报告</p>
- <h1 id="originalReportTitle">原始报告内容</h1>
- <p>${escapeHtml(report?.title || "未命名报告")}</p>
- <p>${escapeHtml(report?.hospitalName || "医院信息待确认")} · ${escapeHtml(report?.departmentName || "科室待确认")}</p>
- <p>出具时间:${escapeHtml(report?.issuedAt || "待确认")}</p>
- </div>
- <section class="report-card" aria-label="原始指标">
- <ul class="indicator-list">
- ${metrics.length ? metrics.map(renderMetric).join("") : '<li class="report-metric"><span>暂无指标数据</span></li>'}
- </ul>
- </section>
- </section>
- `;
- };
- export const renderConsentOverlay = (state = {}) => {
- const reportId = state?.overlays?.reportId;
- const report = state?.reportSourceProjections?.[reportId];
- return `
- <div class="consent-backdrop" role="presentation">
- <section class="consent-sheet" role="dialog" aria-modal="true" aria-labelledby="consentTitle" tabindex="-1">
- <div class="consent-sheet__icon"><i data-lucide="shield-check"></i></div>
- <p class="section-eyebrow">隐私授权</p>
- <h2 id="consentTitle">授权 AI 辅助解释本次报告</h2>
- <dl class="consent-details">
- <div><dt>就诊人</dt><dd>${escapeHtml(patientDisplayName(state))}</dd></div>
- <div><dt>用途</dt><dd>仅用于这份报告的 AI 辅助解释</dd></div>
- <div><dt>范围</dt><dd>${escapeHtml(report?.title || "当前血常规报告")}</dd></div>
- <div><dt>有效期</dt><dd>至 2026-07-09 23:59</dd></div>
- </dl>
- <div class="consent-note">
- <p><strong>AI 辅助性质:</strong>用于帮助理解指标,不替代医生诊断或诊疗建议。</p>
- <p><strong>风险说明:</strong>解释可能存在遗漏或偏差,请结合症状由医生判断。</p>
- <p><strong>撤回方式:</strong>可随时在隐私设置中撤回,撤回后不再展示历史解释。</p>
- </div>
- <div class="consent-actions">
- <button class="button button--primary" type="button" data-action="grant-report-consent">同意并继续</button>
- <button class="button button--secondary" type="button" data-action="reject-report-consent">暂不授权</button>
- </div>
- </section>
- </div>
- `;
- };
- 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 `
- <div class="consent-backdrop command-backdrop" role="presentation">
- <section class="consent-sheet command-sheet command-sheet--${presentation.tone}" role="dialog" aria-modal="true" aria-labelledby="commandTitle" tabindex="-1">
- <div class="command-status">
- <i data-lucide="${presentation.icon}"></i>
- <strong>${statusLabel}</strong>
- </div>
- <p class="section-eyebrow">受控业务操作</p>
- <h2 id="commandTitle">${escapeHtml(details.operationName ?? "确认挂号")}</h2>
- <dl class="command-details">
- <div><dt>就诊人</dt><dd>${escapeHtml(details.patientName)}</dd></div>
- <div><dt>科室 / 医生</dt><dd>${escapeHtml(details.departmentName)} · ${escapeHtml(details.doctorName)}${escapeHtml(details.doctorTitle)}</dd></div>
- <div><dt>就诊时间</dt><dd>${escapeHtml(details.visitAt)}</dd></div>
- <div><dt>金额</dt><dd>${escapeHtml(details.fee)}</dd></div>
- <div><dt>有效时间</dt><dd>${escapeHtml(details.countdown ?? details.expiresAt)}</dd></div>
- </dl>
- ${command?.status === "UNKNOWN" ? `
- <p class="command-guidance">结果确认中,请勿重复操作。系统会继续查询最终结果。</p>
- ` : ""}
- ${command?.status === "FAILED" ? `
- <p class="command-guidance">本次操作未成功。请重新查询号源,不直接重试该 Command。</p>
- ` : ""}
- <div class="consent-actions">
- ${awaiting ? `
- <button class="button button--primary" type="button" data-action="confirm-command">确认挂号</button>
- <button class="button button--secondary" type="button" data-action="reject-command">暂不操作</button>
- ` : ""}
- ${canRequery ? `
- <button class="button button--secondary" type="button" data-action="requery-registration">重新查询号源</button>
- ` : ""}
- ${canClose ? `
- <button class="button button--primary" type="button" data-action="close-command">${command.status === "SUCCEEDED" ? "查看首页事项" : "关闭"}</button>
- ` : ""}
- </div>
- </section>
- </div>
- `;
- };
- export const renderReportInterpreting = () => `
- <section class="report-view report-loading" aria-labelledby="loadingTitle" aria-busy="true">
- <button class="text-button report-view__back" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div class="loading-orbit" aria-hidden="true"><i data-lucide="sparkles"></i></div>
- <p class="section-eyebrow">AI 辅助解释</p>
- <h1 id="loadingTitle">正在整理报告解读</h1>
- <p>正在结合参考范围整理便于理解的信息,请稍候。结果仅供辅助理解,不代表诊断结论。</p>
- <div class="loading-progress" role="progressbar" aria-label="报告解读处理中">
- <span></span>
- </div>
- <div class="loading-step"><i data-lucide="loader-circle"></i>分析仍在进行,暂不提供确定性结论</div>
- </section>
- `;
- 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 `
- <section class="report-view report-unavailable" role="status" aria-labelledby="reportUnavailableTitle">
- <div class="report-unavailable__icon" aria-hidden="true"><i data-lucide="shield-alert"></i></div>
- <h1 id="reportUnavailableTitle">${escapeHtml(presentation.title)}</h1>
- <p>${escapeHtml(presentation.description)}</p>
- ${typeof serviceNotice === "string" && serviceNotice ? `
- <p class="result-service-notice" role="status">${escapeHtml(serviceNotice)}</p>
- ` : ""}
- <div class="report-unavailable__actions">
- ${actions.map(({ action, label, tone }) => `
- <button class="button button--${tone}" type="button" data-action="${escapeHtml(action)}" data-report-id="${escapeHtml(safeReportId)}">${escapeHtml(label)}</button>
- `).join("")}
- </div>
- </section>
- `;
- };
- export const renderReportServiceOverlay = (state = {}) => {
- const reason = state?.overlays?.reportUnavailableReason;
- const presentation = Object.hasOwn(reportUnavailablePresentation, reason)
- ? reportUnavailablePresentation[reason]
- : reportUnavailablePresentation.UNSUPPORTED;
- return `
- <div class="consent-backdrop report-service-backdrop" role="presentation">
- <section class="consent-sheet report-service-sheet" role="dialog" aria-modal="true" aria-labelledby="reportServiceTitle" tabindex="-1">
- <div class="consent-sheet__icon"><i data-lucide="headphones"></i></div>
- <p class="section-eyebrow">人工协助</p>
- <h2 id="reportServiceTitle">人工服务窗口</h2>
- <p>${escapeHtml(presentation.title)}</p>
- <p>${escapeHtml(presentation.description)}</p>
- <div class="consent-note">
- <p>医院服务人员可协助核对报告状态和原始内容。</p>
- <p>本原型仅演示服务入口,不会真实发起电话或在线会话。</p>
- </div>
- <div class="consent-actions">
- <button class="button button--primary" type="button" data-action="close-report-service">知道了</button>
- </div>
- </section>
- </div>
- `;
- };
- 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 `
- <section class="report-view result-view" aria-labelledby="resultTitle">
- <button class="text-button report-view__back" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div class="result-summary">
- <p class="section-eyebrow">AI 辅助解释 · 结果版本 ${escapeHtml(result.resultVersion)}</p>
- <h2>结果概括</h2>
- <h1 id="resultTitle">${escapeHtml(interpretation.headline)}</h1>
- <p>${escapeHtml(interpretation.summary)}</p>
- </div>
- <div class="safety-callout">
- <strong><i data-lucide="shield-check"></i>安全提示</strong>
- <p>${escapeHtml(interpretation.safety)}</p>
- </div>
- <section class="result-card">
- <h2>需要关注</h2>
- <ul>${highlights.map((item) => `
- <li>
- <span>${escapeHtml(item?.name)}</span>
- <strong>${escapeHtml(item?.value)}</strong>
- <em>${escapeHtml(item?.flag)}</em>
- </li>
- `).join("")}</ul>
- </section>
- <section class="result-card">
- <h2>指标解释</h2>
- ${explanations.map((item) => `
- <article class="explanation-item">
- <h3>${escapeHtml(item.indicator)}</h3>
- <p>${escapeHtml(item.explanation)}</p>
- </article>
- `).join("")}
- </section>
- <section class="result-card">
- <h2>常见相关因素</h2>
- <ul>${commonFactors.map((factor) => `<li>${escapeHtml(factor)}</li>`).join("")}</ul>
- <p class="result-factors-note">上述因素只是帮助理解指标的常见情况,不代表你患有相应疾病,也不能作为诊断结果。</p>
- </section>
- <section class="result-card">
- <h2>可以怎么做</h2>
- <ul>${canDo.map((action) => `<li>${escapeHtml(action)}</li>`).join("")}</ul>
- </section>
- <section class="result-card result-evidence">
- <h2>内容依据</h2>
- <ul>${evidence.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
- <p>结果版本 ${escapeHtml(result.resultVersion)} · 对应当前医院原始报告</p>
- </section>
- ${reportId ? `
- <section class="result-card result-operations" aria-labelledby="resultOperationsTitle">
- <h2 id="resultOperationsTitle">需要进一步帮助</h2>
- ${serviceNotice ? `
- <p class="result-service-notice" role="status">${escapeHtml(serviceNotice)}</p>
- ` : ""}
- <div class="result-action-grid">
- <button class="button button--secondary" type="button" ${reportAction("view-original-report")}>查看原始报告</button>
- <button class="button button--secondary" type="button" ${reportAction("consult-ordering-doctor")}>咨询开单医生</button>
- <button class="button button--secondary" type="button" ${reportAction("online-consultation")}>在线咨询</button>
- <button class="button button--secondary" type="button" ${reportAction("contact-human-service")}>联系人工服务</button>
- <button class="button button--secondary" type="button" ${reportAction("open-report-feedback")}>提交反馈</button>
- <button class="button button--secondary result-revoke" type="button" ${reportAction("revoke-report-consent")}>撤回授权</button>
- </div>
- </section>
- ` : ""}
- ${reportId && options?.feedbackOpen ? `
- <section class="result-card report-feedback" aria-labelledby="reportFeedbackTitle">
- <h2 id="reportFeedbackTitle">这次解释对你有帮助吗?</h2>
- <p>请选择最符合的一项,反馈仅用于改进本次辅助解释。</p>
- <div class="feedback-options">
- ${feedbackCategories.map(([category, label]) => `
- <button
- class="button button--secondary"
- type="button"
- data-action="submit-report-feedback"
- data-report-id="${escapeHtml(reportId)}"
- data-filter="${category}"
- >${label}</button>
- `).join("")}
- </div>
- </section>
- ` : ""}
- ${options?.feedbackSubmitted ? `
- <p class="report-feedback-confirmation" role="status">感谢反馈,已记录你的选择。</p>
- ` : ""}
- </section>
- `;
- };
- 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 `
- <section class="report-view queue-view" aria-labelledby="queueTitle">
- <button class="text-button report-view__back" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div class="empty-card">
- <i data-lucide="clock-3"></i>
- <div>
- <h1 id="queueTitle">候诊信息待更新</h1>
- <p>完成签到后,排队号和预计时间会显示在这里。</p>
- </div>
- </div>
- </section>
- `;
- }
- const updatedAt = typeof queue.updatedAt === "string"
- ? queue.updatedAt.slice(11, 16)
- : "待更新";
- return `
- <section class="report-view queue-view" aria-labelledby="queueTitle">
- <button class="text-button report-view__back" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div class="queue-hero">
- <p class="section-eyebrow">神经内科 · 实时候诊</p>
- <h1 id="queueTitle">排队号 ${escapeHtml(queue.queueNumber ?? "待更新")}</h1>
- <p>${escapeHtml(queue.room ?? "诊室待分配")}</p>
- </div>
- ${queue.delayed ? `
- <div class="queue-delay" role="status">
- <i data-lucide="triangle-alert"></i>
- <span>候诊时间可能延后,请留意叫号更新。</span>
- </div>
- ` : ""}
- <div class="queue-metrics">
- <article><span>前方人数</span><strong>前方 ${escapeHtml(queue.peopleAhead ?? 0)} 人</strong></article>
- <article><span>预计时间</span><strong>预计等待 ${escapeHtml(queue.estimatedMinutes ?? 0)} 分钟</strong></article>
- </div>
- <div class="queue-detail">
- <p><i data-lucide="door-open"></i><span>当前诊室</span><strong>${escapeHtml(queue.room ?? "待分配")}</strong></p>
- <p><i data-lucide="refresh-cw"></i><span>队列更新时间</span><strong>${escapeHtml(updatedAt)}</strong></p>
- </div>
- <button class="button button--secondary queue-route" type="button" data-action="open-route" data-appointment-id="${escapeHtml(appointmentId)}">
- <i data-lucide="navigation"></i>院内导航
- </button>
- </section>
- `;
- };
- export const renderRouteView = (location) => `
- <section class="report-view route-view" aria-labelledby="routeTitle">
- <button class="text-button report-view__back" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div class="route-map" aria-hidden="true">
- <i data-lucide="map-pinned"></i>
- <span></span><span></span><span></span>
- </div>
- <div class="module-header">
- <p class="section-eyebrow">院内路线</p>
- <h1 id="routeTitle">${escapeHtml(location ?? "门诊三楼神经内科诊区")}</h1>
- <p>从门诊大厅乘 2 号电梯至三楼,出电梯后右转约 30 米。</p>
- </div>
- <div class="queue-detail">
- <p><i data-lucide="footprints"></i><span>预计步行</span><strong>4 分钟</strong></p>
- <p><i data-lucide="accessibility"></i><span>无障碍路线</span><strong>可用</strong></p>
- </div>
- </section>
- `;
- const pageHeader = (eyebrow, title, description) => `
- <div class="module-header">
- <p class="section-eyebrow">${eyebrow}</p>
- <h1>${title}</h1>
- <p>${description}</p>
- </div>
- `;
- 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 `
- <section class="module-page" aria-labelledby="careOverviewTitle">
- ${pageHeader("就医总览", "我的就医", "按办理状态集中查看门诊、检查和诊后事项。")}
- <div class="care-groups">
- ${careGroupDefinitions.map((group) => {
- const matches = items.filter((item) => (
- group.statuses.includes(item.status)
- ));
- const visible = matches.length > 0
- ? matches
- : [{ title: group.fallback, description: "进入详情查看下一步" }];
- return `
- <section class="care-group">
- <div class="section-heading">
- <h2>${group.title}</h2>
- <span>${visible.length}</span>
- </div>
- ${visible.slice(0, 3).map((item) => `
- <article class="compact-row">
- <span class="compact-row__icon"><i data-lucide="clipboard-check"></i></span>
- <div>
- <strong>${escapeHtml(item.title)}</strong>
- <p>${escapeHtml(item.description ?? "状态和可用操作持续更新")}</p>
- </div>
- <i data-lucide="chevron-right"></i>
- </article>
- `).join("")}
- </section>
- `;
- }).join("")}
- </div>
- </section>
- `;
- };
- 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 `
- <section class="module-page" aria-labelledby="recordsTitle">
- ${pageHeader("健康档案", "我的健康记录", "报告、门诊记录和授权信息统一归档。")}
- <div class="filter-tabs" role="group" aria-label="报告筛选">
- ${[
- ["all", "全部"],
- ["abnormal", "异常"],
- ["unread", "未解读"],
- ].map(([id, label]) => `
- <button type="button" data-action="set-record-filter" data-filter="${id}" aria-pressed="${activeFilter === id}">
- ${label}
- </button>
- `).join("")}
- </div>
- <div class="module-list">
- ${visible.map((entry) => `
- <${entry.action ? "button" : "article"} class="compact-row"${entry.action ? ` type="button" data-action="${entry.action}" data-filter="${entry.reportFamily}"` : ""}>
- <span class="compact-row__icon"><i data-lucide="${entry.icon}"></i></span>
- <div><strong>${entry.label}</strong><p>${entry.meta}</p></div>
- <i data-lucide="chevron-right"></i>
- </${entry.action ? "button" : "article"}>
- `).join("")}
- </div>
- </section>
- `;
- };
- const profileEntries = [
- ["就诊人管理", "users-round"],
- ["授权中心", "shield-check"],
- ["消息设置", "bell-ring"],
- ["常用院区", "hospital"],
- ["医保和支付", "badge-cent"],
- ["电子票据", "receipt-text"],
- ["人工客服", "headphones"],
- ["关于 AI", "sparkles"],
- ];
- export const renderProfile = () => `
- <section class="module-page" aria-labelledby="profileTitle">
- ${pageHeader("个人中心", "我的", "管理就诊人、隐私授权和常用服务设置。")}
- <div class="module-list">
- ${profileEntries.map(([label, icon]) => `
- <button class="compact-row compact-row--button" type="button">
- <span class="compact-row__icon"><i data-lucide="${icon}"></i></span>
- <strong>${label}</strong>
- <i data-lucide="chevron-right"></i>
- </button>
- `).join("")}
- </div>
- </section>
- `;
- const messageCategories = ["报告", "就医", "缴费", "随访", "系统"];
- export const renderMessages = (state) => {
- const messages = Array.isArray(state?.messages)
- ? state.messages.filter((message) => message && typeof message === "object")
- : [];
- return `
- <section class="module-page" aria-labelledby="messagesTitle">
- ${pageHeader("消息中心", "消息", "按服务类型定位到对应就医事项,不会直接执行办理操作。")}
- <div class="message-categories" aria-label="消息类型">
- ${messageCategories.map((category) => `<span>${category}</span>`).join("")}
- </div>
- <div class="module-list">
- ${messages.length > 0 ? messages.map((message) => `
- <button class="compact-row compact-row--button" type="button" data-action="open-message" data-message-id="${escapeHtml(message.id)}">
- <span class="compact-row__icon"><i data-lucide="${message.type === "REPORT" ? "file-chart-column" : "bell"}"></i></span>
- <div>
- <strong>${escapeHtml(message.title ?? message.text ?? "服务消息")}</strong>
- <p>${escapeHtml(message.createdAt ?? "刚刚更新")}</p>
- </div>
- <i data-lucide="chevron-right"></i>
- </button>
- `).join("") : `
- <div class="empty-card">
- <i data-lucide="inbox"></i>
- <div><strong>暂无新消息</strong><p>新的报告和就医提醒会显示在这里。</p></div>
- </div>
- `}
- </div>
- </section>
- `;
- };
- 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 `
- <section class="system-state" role="status">
- <span><i data-lucide="${icon}"></i></span>
- <h1>${title}</h1>
- <p>${description}</p>
- <button class="button button--secondary" type="button" data-action="go-home">返回首页</button>
- </section>
- `;
- };
- 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) => `
- <section
- class="scenario-group"
- aria-labelledby="${groupId}"
- >
- <h2 id="${groupId}">${title}</h2>
- <div class="scenario-list">
- ${scenarios.map((scenario) => `
- <button type="button" data-action="load-scenario" data-scenario="${scenario}" aria-pressed="${scenario === activeScenario}">
- <span>${String(demoScenarios.indexOf(scenario) + 1).padStart(2, "0")}</span>
- ${scenarioLabels[scenario]}
- </button>
- `).join("")}
- </div>
- </section>
- `;
- return `
- <div class="scenario-controller">
- <div class="scenario-controller__header">
- <div>
- <p>方案演示</p>
- <strong>${escapeHtml(scenarioLabels[activeScenario])}</strong>
- </div>
- <button type="button" data-action="toggle-scenario-panel" aria-label="隐藏演示控制器">
- <i data-lucide="panel-right-close"></i>
- </button>
- </div>
- <div class="scenario-actions">
- <button type="button" data-action="previous-scenario"><i data-lucide="arrow-left"></i>上一步</button>
- <button type="button" data-action="next-scenario">下一步<i data-lucide="arrow-right"></i></button>
- <button type="button" data-action="reset-scenario"><i data-lucide="rotate-ccw"></i>重置</button>
- </div>
- <div class="scenario-groups" aria-label="演示场景">
- ${renderScenarioGroup(
- "reportClosureScenarios",
- "报告闭环",
- demoScenarioGroups.REPORT_CLOSURE,
- )}
- ${renderScenarioGroup(
- "careClosureScenarios",
- "就医闭环",
- demoScenarioGroups.CARE_CLOSURE,
- )}
- </div>
- <div class="viewport-switcher" aria-label="演示视口">
- <button type="button" data-action="set-demo-viewport" data-viewport="390">390×844</button>
- <button type="button" data-action="set-demo-viewport" data-viewport="430">430×932</button>
- </div>
- <dl class="projection-summary">
- <div><dt>当前 fixture</dt><dd>${escapeHtml(latestFixture)}</dd></div>
- <div><dt>Agent task</dt><dd>${escapeHtml(activeTask ? `${activeTask.type}/${activeTask.stage}` : "无")}</dd></div>
- <div><dt>PatientWorkItem</dt><dd>${escapeHtml(state?.patientWorkItems?.length ?? 0)} 项</dd></div>
- <div><dt>Command</dt><dd>${escapeHtml(commands.map((item) => `${item.commandId}/${item.status}`).join(", ") || "无")}</dd></div>
- </dl>
- <p class="scenario-shortcuts">←/→ 切换 · R 重置 · P 显示/隐藏</p>
- </div>
- `;
- };
- 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 `
- <section class="report-view checkin-view" aria-labelledby="checkinTitle">
- <button class="text-button report-view__back" type="button" data-action="go-home">
- <i data-lucide="arrow-left"></i>返回首页
- </button>
- <div class="report-hero">
- <p class="section-eyebrow">到院签到</p>
- <h1 id="checkinTitle">请确认本人已到达诊区</h1>
- <p>签到需要患者明确确认;本页面不会根据位置自动完成签到。</p>
- </div>
- <div class="report-notice">
- <i data-lucide="map-pin-check"></i>
- <p>确认后仍以医院签到接口返回结果为准。</p>
- </div>
- <div class="task-card__actions">
- <button class="button button--primary" type="button" data-action="submit-checkin" data-appointment-id="${escapeHtml(state.overlays.appointmentId)}">
- 确认签到
- </button>
- <button class="button button--secondary" type="button" data-action="go-home">
- 暂不签到
- </button>
- </div>
- </section>
- `;
- }
- 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 }) => `
- <button
- class="nav-item"
- type="button"
- data-nav-id="${id}"
- data-action="navigate-tab"
- data-tab-id="${id}"
- aria-label="${label}"
- ${state?.activeTab === id ? 'aria-current="page"' : ""}
- >
- <i data-lucide="${icon}"></i>
- <span>${label}</span>
- </button>
- `,
- ).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();
- }
- }
|