patient-service-desk-v3.bundle.js 174 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936
  1. /* Generated from patient-service-desk-v3.js for direct file:// opening. */
  2. (() => {
  3. // docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs
  4. function createInitialState({
  5. activePatientId = "patient-zhang-san",
  6. demoNow = "2026-07-03T09:00:00+08:00"
  7. } = {}) {
  8. return {
  9. activeTab: "home",
  10. activePatientId,
  11. demoNow,
  12. appliedProjectionFixtureIds: [],
  13. activeAgentTaskProjection: null,
  14. patientWorkItems: [],
  15. journeyProjections: [],
  16. reportSourceProjections: {},
  17. reportFeedback: [],
  18. reportFilters: { family: "ALL", status: "ALL" },
  19. resultSnapshotProjections: {},
  20. commandProjections: {},
  21. messages: [],
  22. consents: {},
  23. consentScopeBindings: {},
  24. overlays: {
  25. commandId: null,
  26. consentId: null,
  27. consentScope: null,
  28. reportId: null,
  29. reportView: null,
  30. reportUnavailableReason: null,
  31. reportExpanded: false,
  32. reportFeedbackOpen: false,
  33. reportFeedbackSubmitted: false,
  34. reportServiceNotice: null,
  35. appointmentId: null,
  36. journeyView: null,
  37. scenarioPanelOpen: false
  38. }
  39. };
  40. }
  41. var deepFreeze = (value) => {
  42. if (value && typeof value === "object" && !Object.isFrozen(value)) {
  43. Object.freeze(value);
  44. Object.values(value).forEach(deepFreeze);
  45. }
  46. return value;
  47. };
  48. var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
  49. var reportIdPattern = /^[^\s\u0000-\u001F\u007F]{1,128}$/;
  50. var reportHashPattern = /^[^\s\u0000-\u001F\u007F]{1,256}$/;
  51. var reportSourceStatuses = /* @__PURE__ */ new Set(["FINAL", "PRELIMINARY"]);
  52. var reportFamilies = /* @__PURE__ */ new Set(["LAB", "EXAM"]);
  53. var reportTypes = /* @__PURE__ */ new Set(["BLOOD_ROUTINE", "IMAGING"]);
  54. var reportVerificationStatuses = /* @__PURE__ */ new Set(["VERIFIED", "NEED_REVIEW"]);
  55. var bloodRoutineReportFixture = deepFreeze({
  56. reportId: "report-blood-20260702",
  57. family: "LAB",
  58. reportType: "BLOOD_ROUTINE",
  59. title: "\u8840\u5E38\u89C4",
  60. hospitalName: "\u7A7A\u6D77\u533B\u9662",
  61. departmentName: "\u68C0\u9A8C\u79D1",
  62. issuedAt: "2026-07-03 09:30",
  63. sourceStatus: "FINAL",
  64. interpretationStatus: "NOT_REQUESTED",
  65. verificationStatus: "VERIFIED",
  66. sourceVersion: 1,
  67. sourceHash: "sha256:blood-v1",
  68. readStatus: "UNREAD",
  69. metrics: [
  70. {
  71. code: "WBC",
  72. name: "\u767D\u7EC6\u80DE\u8BA1\u6570",
  73. value: 11.2,
  74. unit: "10^9/L",
  75. referenceRange: "3.5\u20139.5",
  76. sourceFlag: "HIGH"
  77. }
  78. ]
  79. });
  80. var registrationDemoText = "\u6211\u60F3\u6302\u660E\u5929\u795E\u7ECF\u5185\u79D1\u674E\u660E\u4E3B\u4EFB\u7684\u53F7";
  81. var registrationResultRef = "result-slot-001";
  82. var registrationResultVersion = 1;
  83. var registrationItems = deepFreeze([
  84. {
  85. candidateId: "candidate-slot-0930",
  86. visitDate: "2026-07-03",
  87. timePeriod: "\u4E0A\u5348",
  88. visitTime: "09:30",
  89. departmentName: "\u795E\u7ECF\u5185\u79D1",
  90. doctorName: "\u674E\u660E",
  91. doctorTitle: "\u4E3B\u4EFB\u533B\u5E08",
  92. remainingSlots: 3,
  93. fee: "30\u5143"
  94. },
  95. {
  96. candidateId: "candidate-slot-1030",
  97. visitDate: "2026-07-03",
  98. timePeriod: "\u4E0A\u5348",
  99. visitTime: "10:30",
  100. departmentName: "\u795E\u7ECF\u5185\u79D1",
  101. doctorName: "\u674E\u660E",
  102. doctorTitle: "\u4E3B\u4EFB\u533B\u5E08",
  103. remainingSlots: 1,
  104. fee: "30\u5143"
  105. },
  106. {
  107. candidateId: "candidate-slot-1430",
  108. visitDate: "2026-07-03",
  109. timePeriod: "\u4E0B\u5348",
  110. visitTime: "14:30",
  111. departmentName: "\u795E\u7ECF\u5185\u79D1",
  112. doctorName: "\u674E\u660E",
  113. doctorTitle: "\u4E3B\u4EFB\u533B\u5E08",
  114. remainingSlots: 5,
  115. fee: "30\u5143"
  116. }
  117. ]);
  118. var registrationResultText = [
  119. `\u5DF2\u4E3A\u4F60\u627E\u5230 ${registrationItems[0].visitDate} ${registrationItems[0].departmentName}${registrationItems[0].doctorName}${registrationItems[0].doctorTitle}\u7684 ${registrationItems.length} \u4E2A\u53F7\u6E90\uFF1A`,
  120. ...registrationItems.map((item) => `${item.visitDate} ${item.timePeriod} ${item.visitTime}\uFF0C${item.departmentName}\uFF0C${item.doctorName}${item.doctorTitle}\uFF0C\u4F59\u53F7 ${item.remainingSlots}\uFF0C\u6302\u53F7\u8D39 ${item.fee}\uFF1B`),
  121. "\u8BF7\u9009\u62E9\u4E00\u4E2A\u53F7\u6E90\u3002"
  122. ].join("");
  123. var nextMessageId = (messages, role) => {
  124. const usedIds = new Set(
  125. messages.filter((message) => message && typeof message === "object").map(({ id }) => id)
  126. );
  127. let sequence = messages.length + 1;
  128. while (usedIds.has(`message-${role}-${sequence}`)) {
  129. sequence += 1;
  130. }
  131. return `message-${role}-${sequence}`;
  132. };
  133. var simulateRegistrationQueryBackendFixture = (interaction) => {
  134. if (interaction?.text !== registrationDemoText) {
  135. return {
  136. type: "NO_RESULT",
  137. text: interaction?.text ? "\u6682\u65F6\u6CA1\u6709\u627E\u5230\u5339\u914D\u7ED3\u679C\u3002\u4F60\u53EF\u4EE5\u6362\u4E2A\u8BF4\u6CD5\uFF0C\u8BD5\u8BD5\u667A\u80FD\u5BFC\u8BCA\uFF0C\u6216\u8F6C\u4EBA\u5DE5\u670D\u52A1\u3002" : "\u8BF7\u8F93\u5165\u60F3\u529E\u7406\u7684\u5C31\u533B\u670D\u52A1\u3002\u4F60\u4E5F\u53EF\u4EE5\u8BD5\u8BD5\u667A\u80FD\u5BFC\u8BCA\u6216\u4EBA\u5DE5\u670D\u52A1\u3002"
  138. };
  139. }
  140. return {
  141. type: "REGISTRATION_CANDIDATES",
  142. task: {
  143. taskId: "agent-task-registration-001",
  144. type: "REGISTRATION",
  145. stage: "CANDIDATE_SELECTION",
  146. constraints: {
  147. departmentName: "\u795E\u7ECF\u5185\u79D1",
  148. doctorName: "\u674E\u660E",
  149. doctorTitle: "\u4E3B\u4EFB\u533B\u5E08",
  150. visitDate: "2026-07-03"
  151. },
  152. missingFields: [],
  153. activeResultRef: registrationResultRef
  154. },
  155. result: {
  156. resultRef: registrationResultRef,
  157. resultVersion: registrationResultVersion,
  158. expiresAt: "2026-07-03T10:00:00+08:00",
  159. text: registrationResultText,
  160. items: registrationItems.map((item) => ({ ...item }))
  161. }
  162. };
  163. };
  164. var applyRegistrationQueryResponseFixture = (state, interaction, fixture) => {
  165. const messages = Array.isArray(state.messages) ? state.messages : [];
  166. const userMessage = interaction.text ? [{
  167. id: nextMessageId(messages, "user"),
  168. role: "user",
  169. text: interaction.text
  170. }] : [];
  171. const assistantMessage = {
  172. id: nextMessageId([...messages, ...userMessage], "assistant"),
  173. role: "assistant",
  174. text: fixture.text ?? fixture.result?.text,
  175. ...fixture.result?.resultRef ? { resultRef: fixture.result.resultRef } : {}
  176. };
  177. if (fixture.type !== "REGISTRATION_CANDIDATES") {
  178. return {
  179. ...state,
  180. activeTab: "assistant",
  181. messages: [...messages, ...userMessage, assistantMessage]
  182. };
  183. }
  184. return {
  185. ...state,
  186. activeTab: "assistant",
  187. activeAgentTaskProjection: fixture.task,
  188. resultSnapshotProjections: {
  189. ...state.resultSnapshotProjections ?? {},
  190. [fixture.result.resultRef]: fixture.result
  191. },
  192. messages: [...messages, ...userMessage, assistantMessage]
  193. };
  194. };
  195. function submitInteraction(state, text) {
  196. if (!state || typeof state !== "object" || Array.isArray(state)) {
  197. return state;
  198. }
  199. const normalizedText = typeof text === "string" ? text.trim() : "";
  200. const interaction = {
  201. interactionType: "USER_MESSAGE",
  202. text: normalizedText
  203. };
  204. const responseFixture = simulateRegistrationQueryBackendFixture(
  205. interaction
  206. );
  207. return applyRegistrationQueryResponseFixture(
  208. state,
  209. interaction,
  210. responseFixture
  211. );
  212. }
  213. function buildCandidateSelectionInteraction(state, candidateSelection, now) {
  214. const task = state?.activeAgentTaskProjection;
  215. const resultRef = task?.activeResultRef;
  216. const result = typeof resultRef === "string" ? state?.resultSnapshotProjections?.[resultRef] : null;
  217. const normalizedSelection = typeof candidateSelection === "string" ? candidateSelection.trim() : candidateSelection;
  218. const candidateId = normalizedSelection === "\u9009\u7B2C\u4E00\u4E2A" ? result?.items?.[0]?.candidateId : normalizedSelection;
  219. const expiresAt = Date.parse(result?.expiresAt);
  220. const evaluatedAt = Date.parse(now ?? state?.demoNow);
  221. const isExpired = !Number.isFinite(expiresAt) || !Number.isFinite(evaluatedAt) || expiresAt <= evaluatedAt;
  222. const hasCandidate = Array.isArray(result?.items) && result.items.some(
  223. (item) => item?.candidateId === candidateId
  224. );
  225. if (task?.type !== "REGISTRATION" || task.stage !== "CANDIDATE_SELECTION" || result?.resultRef !== resultRef || !Number.isInteger(result?.resultVersion) || result.resultVersion <= 0 || isExpired || !hasCandidate) {
  226. return null;
  227. }
  228. return {
  229. interactionType: "SELECT_CANDIDATE",
  230. resultRef,
  231. resultVersion: result.resultVersion,
  232. candidateId
  233. };
  234. }
  235. function applyCandidateSelectionResponseFixture(state, fixture) {
  236. if (!state || typeof state !== "object" || !fixture?.id) {
  237. return state;
  238. }
  239. const appliedIds = Array.isArray(state.appliedProjectionFixtureIds) ? state.appliedProjectionFixtureIds : [];
  240. if (appliedIds.includes(fixture.id)) {
  241. return state;
  242. }
  243. const task = state?.activeAgentTaskProjection;
  244. const result = task?.activeResultRef ? state?.resultSnapshotProjections?.[task.activeResultRef] : null;
  245. const isActiveSelection = task?.type === "REGISTRATION" && task.stage === "CANDIDATE_SELECTION" && result?.resultRef === task.activeResultRef;
  246. if (!isActiveSelection) {
  247. return state;
  248. }
  249. const canonicalErrors = /* @__PURE__ */ new Set([
  250. "RESULT_VERSION_CONFLICT",
  251. "RESULT_EXPIRED",
  252. "SLOT_UNAVAILABLE"
  253. ]);
  254. const validation = fixture.validation ?? {};
  255. const derivedError = validation.versionMatched === false ? "RESULT_VERSION_CONFLICT" : validation.notExpired === false ? "RESULT_EXPIRED" : validation.realtimeAvailable === false || validation.l1LockSucceeded === false ? "SLOT_UNAVAILABLE" : null;
  256. const baseState = {
  257. ...state,
  258. appliedProjectionFixtureIds: [...appliedIds, fixture.id]
  259. };
  260. const evaluatedAt = Date.parse(state.demoNow);
  261. const snapshotExpiresAt = Date.parse(result.expiresAt);
  262. const snapshotNotExpired = Number.isFinite(evaluatedAt) && Number.isFinite(snapshotExpiresAt) && snapshotExpiresAt > evaluatedAt;
  263. const lockExpiresAt = Date.parse(fixture.lock?.expiresAt);
  264. const lockIdValid = typeof fixture.lock?.lockId === "string" && fixture.lock.lockId.trim().length > 0;
  265. const lockExpiryValid = Number.isFinite(evaluatedAt) && Number.isFinite(lockExpiresAt) && lockExpiresAt > evaluatedAt;
  266. const candidateExists = Array.isArray(result?.items) && result.items.some(
  267. (item) => item?.candidateId === fixture.candidateId
  268. );
  269. const validationPassed = [
  270. "tenantMatched",
  271. "patientMatched",
  272. "sessionMatched",
  273. "versionMatched",
  274. "notExpired",
  275. "realtimeAvailable",
  276. "l1LockSucceeded"
  277. ].every((key) => validation[key] === true);
  278. const successIsValid = fixture.outcome === "SUCCESS" && fixture.resultRef === result.resultRef && fixture.resultVersion === result.resultVersion && candidateExists && validationPassed && snapshotNotExpired && lockIdValid && lockExpiryValid;
  279. if (!successIsValid) {
  280. const errorCode = canonicalErrors.has(fixture.errorCode) ? fixture.errorCode : derivedError ?? (!snapshotNotExpired ? "RESULT_EXPIRED" : !lockIdValid && fixture.outcome === "SUCCESS" ? "SLOT_UNAVAILABLE" : !lockExpiryValid && fixture.outcome === "SUCCESS" ? "RESULT_EXPIRED" : "UNKNOWN_SELECTION_ERROR");
  281. const unavailableAdvice = errorCode === "SLOT_UNAVAILABLE" ? " \u5F53\u524D\u53F7\u6E90\u5DF2\u65E0\u53F7\uFF0C\u53EF\u67E5\u770B\u5176\u4ED6\u65F6\u6BB5\u6216\u533B\u751F\u3002" : "";
  282. const messages2 = Array.isArray(state.messages) ? state.messages : [];
  283. return {
  284. ...baseState,
  285. activeAgentTaskProjection: {
  286. ...task,
  287. stage: "REQUERY_REQUIRED",
  288. activeResultRef: null
  289. },
  290. messages: [
  291. ...messages2,
  292. {
  293. id: nextMessageId(messages2, "assistant"),
  294. role: "assistant",
  295. text: `\u7ED3\u679C\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u3002${unavailableAdvice}`
  296. }
  297. ]
  298. };
  299. }
  300. const messages = Array.isArray(state.messages) ? state.messages : [];
  301. return {
  302. ...baseState,
  303. activeAgentTaskProjection: {
  304. ...task,
  305. stage: "AWAITING_REGISTRATION_CONFIRMATION",
  306. selectedCandidateId: fixture.candidateId,
  307. lock: { ...fixture.lock }
  308. },
  309. messages: [
  310. ...messages,
  311. {
  312. id: nextMessageId(messages, "assistant"),
  313. role: "assistant",
  314. text: "\u9501\u53F7\u6210\u529F\uFF0C\u8BF7\u5728\u9501\u5B9A\u65F6\u95F4\u5185\u5B8C\u6210\u4E8C\u6B21\u786E\u8BA4\u540E\u6302\u53F7\u3002"
  315. }
  316. ]
  317. };
  318. }
  319. var commandStatuses = /* @__PURE__ */ new Set([
  320. "PREPARED",
  321. "AWAITING_CONFIRMATION",
  322. "EXECUTING",
  323. "SUCCEEDED",
  324. "FAILED",
  325. "EXPIRED",
  326. "UNKNOWN",
  327. "REJECTED"
  328. ]);
  329. var commandTransitions = {
  330. L1: {
  331. PREPARED: /* @__PURE__ */ new Set(["EXECUTING"]),
  332. EXECUTING: /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED", "UNKNOWN"]),
  333. UNKNOWN: /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED"])
  334. },
  335. L2: {
  336. PREPARED: /* @__PURE__ */ new Set(["AWAITING_CONFIRMATION"]),
  337. AWAITING_CONFIRMATION: /* @__PURE__ */ new Set([
  338. "EXECUTING",
  339. "REJECTED",
  340. "EXPIRED"
  341. ]),
  342. EXECUTING: /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED", "UNKNOWN"]),
  343. UNKNOWN: /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED"])
  344. }
  345. };
  346. var initialCommandStatuses = {
  347. L1: /* @__PURE__ */ new Set(["PREPARED", "UNKNOWN"]),
  348. L2: /* @__PURE__ */ new Set(["PREPARED", "AWAITING_CONFIRMATION", "UNKNOWN"])
  349. };
  350. var commandFixtureError = (message) => {
  351. throw new Error(`Command projection fixture invalid: ${message}`);
  352. };
  353. var legacyAppointmentResult = {
  354. type: "APPOINTMENT_CREATED",
  355. eventId: "appointment-created-001",
  356. appointmentId: "appointment-001",
  357. patientId: "patient-zhang-san",
  358. summary: {
  359. visitDate: "2026-07-03",
  360. visitTime: "09:30",
  361. departmentName: "\u795E\u7ECF\u5185\u79D1",
  362. doctorName: "\u674E\u660E",
  363. doctorTitle: "\u4E3B\u4EFB\u533B\u5E08",
  364. fee: "30\u5143"
  365. }
  366. };
  367. var applyAppointmentCreatedProjection = (state, fixture) => {
  368. const result = fixture.result ?? (fixture.resultFixtureId === "appointment-created-001" ? legacyAppointmentResult : null);
  369. if (!result || result.type !== "APPOINTMENT_CREATED") {
  370. return state;
  371. }
  372. const eventId = result.eventId ?? fixture.resultFixtureId;
  373. if (typeof eventId !== "string" || !eventId || typeof result.appointmentId !== "string" || !result.appointmentId) {
  374. commandFixtureError("appointment result identifiers are required");
  375. }
  376. if (state.appliedProjectionFixtureIds.includes(eventId)) {
  377. return state;
  378. }
  379. const summary = result.summary ?? {};
  380. const doctor = `${summary.doctorName ?? ""}${summary.doctorTitle ?? ""}`;
  381. return {
  382. ...state,
  383. appliedProjectionFixtureIds: [
  384. ...state.appliedProjectionFixtureIds,
  385. eventId
  386. ],
  387. patientWorkItems: [
  388. ...state.patientWorkItems,
  389. {
  390. id: `work-item-${result.appointmentId}`,
  391. appointmentId: result.appointmentId,
  392. patientId: result.patientId,
  393. type: "APPOINTMENT",
  394. status: "UPCOMING",
  395. stage: "WAITING_ARRIVAL",
  396. priority: 80,
  397. title: `${summary.departmentName ?? "\u95E8\u8BCA"}\u9884\u7EA6\u6210\u529F`,
  398. description: `${summary.visitDate ?? ""} ${summary.visitTime ?? ""} \xB7 ${doctor} \xB7 ${summary.fee ?? ""}`,
  399. fee: summary.fee,
  400. primaryAction: null,
  401. secondaryAction: null,
  402. expiresAt: null
  403. }
  404. ],
  405. journeyProjections: [
  406. ...state.journeyProjections,
  407. {
  408. id: `journey-${result.appointmentId}`,
  409. stage: "WAITING_ARRIVAL",
  410. title: "\u5F85\u5230\u9662",
  411. appointmentId: result.appointmentId,
  412. patientId: result.patientId
  413. }
  414. ]
  415. };
  416. };
  417. function applyCommandProjectionFixture(state, fixture) {
  418. if (!state || typeof state !== "object" || Array.isArray(state)) {
  419. commandFixtureError("state is required");
  420. }
  421. if (!fixture || typeof fixture !== "object" || Array.isArray(fixture)) {
  422. commandFixtureError("fixture is required");
  423. }
  424. const { id, commandId, status } = fixture;
  425. if (typeof id !== "string" || !id.trim()) {
  426. commandFixtureError("id is required");
  427. }
  428. if (typeof commandId !== "string" || !commandId.trim()) {
  429. commandFixtureError("commandId is required");
  430. }
  431. if (status === "COMPLETED") {
  432. commandFixtureError("COMPLETED is not a Command status");
  433. }
  434. if (!commandStatuses.has(status)) {
  435. commandFixtureError(`unknown status ${String(status)}`);
  436. }
  437. const appliedIds = Array.isArray(state.appliedProjectionFixtureIds) ? state.appliedProjectionFixtureIds : [];
  438. if (appliedIds.includes(id)) {
  439. return state;
  440. }
  441. const current = state.commandProjections?.[commandId];
  442. const riskLevel = fixture.riskLevel ?? current?.riskLevel ?? "L2";
  443. if (riskLevel !== "L1" && riskLevel !== "L2") {
  444. commandFixtureError(`unknown riskLevel ${String(riskLevel)}`);
  445. }
  446. if (current && current.riskLevel !== riskLevel) {
  447. commandFixtureError("riskLevel cannot change");
  448. }
  449. const version = fixture.version ?? current?.version;
  450. if (version !== void 0 && (!Number.isInteger(version) || version <= 0)) {
  451. commandFixtureError("version must be a positive integer");
  452. }
  453. if (!current && !initialCommandStatuses[riskLevel].has(status)) {
  454. commandFixtureError(
  455. `illegal initial ${riskLevel} status ${status}`
  456. );
  457. }
  458. if (current) {
  459. const isFinalBusinessSnapshot = riskLevel === "L2" && current.status === "AWAITING_CONFIRMATION" && status === "SUCCEEDED" && fixture.resultFixtureId === "appointment-created-001" && fixture.result === void 0 && fixture.version === void 0;
  460. const transitionAllowed = commandTransitions[riskLevel][current.status]?.has(status);
  461. if (!isFinalBusinessSnapshot && (!Number.isInteger(fixture.version) || !Number.isInteger(current.version) || fixture.version <= current.version)) {
  462. commandFixtureError("version must explicitly increase");
  463. }
  464. if (!transitionAllowed && !isFinalBusinessSnapshot) {
  465. commandFixtureError(
  466. `illegal ${riskLevel} transition ${current.status} -> ${status}`
  467. );
  468. }
  469. }
  470. const projection = {
  471. ...current,
  472. commandId,
  473. status,
  474. riskLevel,
  475. ...version === void 0 ? {} : { version },
  476. ...fixture.details === void 0 ? {} : { details: structuredClone(fixture.details) },
  477. ...fixture.resultFixtureId === void 0 ? {} : { resultFixtureId: fixture.resultFixtureId },
  478. ...fixture.result === void 0 ? {} : { result: structuredClone(fixture.result) }
  479. };
  480. let nextState = {
  481. ...state,
  482. appliedProjectionFixtureIds: [...appliedIds, id],
  483. commandProjections: {
  484. ...state.commandProjections ?? {},
  485. [commandId]: projection
  486. },
  487. overlays: {
  488. ...state.overlays,
  489. commandId
  490. }
  491. };
  492. if (status === "SUCCEEDED" && (fixture.result?.type === "APPOINTMENT_CREATED" || fixture.resultFixtureId === "appointment-created-001")) {
  493. nextState = applyAppointmentCreatedProjection(nextState, fixture);
  494. }
  495. return nextState;
  496. }
  497. function buildCommandConfirmationRequest(state, commandId, { channel, idempotencyKey } = {}) {
  498. const command = state?.commandProjections?.[commandId];
  499. if (!command) {
  500. throw new Error("Command \u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u786E\u8BA4");
  501. }
  502. if (command.status === "UNKNOWN") {
  503. throw new Error("\u7ED3\u679C\u786E\u8BA4\u4E2D\uFF0C\u8BF7\u52FF\u91CD\u590D\u64CD\u4F5C");
  504. }
  505. if (command.status === "EXPIRED") {
  506. throw new Error("Command \u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u53F7\u6E90");
  507. }
  508. if (command.status !== "AWAITING_CONFIRMATION") {
  509. throw new Error(`\u5F53\u524D\u72B6\u6001 ${command.status} \u4E0D\u5141\u8BB8\u91CD\u590D\u786E\u8BA4`);
  510. }
  511. if (!Number.isInteger(command.version) || command.version <= 0) {
  512. throw new Error("Command version \u65E0\u6548");
  513. }
  514. if (channel !== "CARD") {
  515. throw new Error("\u786E\u8BA4\u6E20\u9053\u4E0D\u53D7\u652F\u6301");
  516. }
  517. if (typeof idempotencyKey !== "string" || !idempotencyKey.trim()) {
  518. throw new Error("\u5E42\u7B49\u952E\u4E0D\u80FD\u4E3A\u7A7A");
  519. }
  520. return {
  521. path: `/api/v2/commands/${encodeURIComponent(commandId)}/confirm`,
  522. method: "POST",
  523. commandId,
  524. body: {
  525. commandVersion: command.version,
  526. confirmation: {
  527. channel,
  528. explicit: true
  529. }
  530. },
  531. headers: {
  532. "X-Emoon-Idempotency-Key": idempotencyKey
  533. }
  534. };
  535. }
  536. function buildCommandRejectionRequest(state, commandId, { reason, idempotencyKey } = {}) {
  537. const command = state?.commandProjections?.[commandId];
  538. if (!command) {
  539. throw new Error("Command \u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u62D2\u7EDD");
  540. }
  541. if (command.status !== "AWAITING_CONFIRMATION") {
  542. throw new Error(`\u5F53\u524D\u72B6\u6001 ${command.status} \u4E0D\u5141\u8BB8\u62D2\u7EDD`);
  543. }
  544. if (!Number.isInteger(command.version) || command.version <= 0) {
  545. throw new Error("Command version \u65E0\u6548");
  546. }
  547. if (typeof idempotencyKey !== "string" || !idempotencyKey.trim()) {
  548. throw new Error("\u5E42\u7B49\u952E\u4E0D\u80FD\u4E3A\u7A7A");
  549. }
  550. return {
  551. path: `/api/v2/commands/${encodeURIComponent(commandId)}/reject`,
  552. method: "POST",
  553. commandId,
  554. body: {
  555. commandVersion: command.version,
  556. ...typeof reason === "string" && reason.trim() ? { reason: reason.trim() } : {}
  557. },
  558. headers: {
  559. "X-Emoon-Idempotency-Key": idempotencyKey
  560. }
  561. };
  562. }
  563. var statusRank = {
  564. ACTION_REQUIRED: 0,
  565. IN_PROGRESS: 1,
  566. UPCOMING: 2,
  567. COMPLETED: 3
  568. };
  569. var reportInterpretationProjectionFixture = deepFreeze({
  570. resultRef: "result-report-interpretation-001",
  571. resultVersion: 1,
  572. interpretation: {
  573. headline: "3 \u9879\u6307\u6807\u8F7B\u5EA6\u5F02\u5E38",
  574. summary: "\u8FD9\u4EFD\u62A5\u544A\u6709 3 \u9879\u6307\u6807\u8D85\u51FA\u53C2\u8003\u8303\u56F4\uFF0C\u53D8\u5316\u5E45\u5EA6\u8F83\u5C0F\uFF0C\u9700\u8981\u7ED3\u5408\u8FD1\u671F\u75C7\u72B6\u548C\u75C5\u53F2\u7531\u533B\u751F\u7EFC\u5408\u5224\u65AD\u3002",
  575. safety: "\u672C\u89E3\u91CA\u4EC5\u7528\u4E8E\u5E2E\u52A9\u7406\u89E3\u62A5\u544A\u6307\u6807\uFF0C\u4E0D\u7528\u4E8E\u5224\u65AD\u75BE\u75C5\u6216\u66FF\u4EE3\u533B\u751F\u610F\u89C1\u3002\u5982\u6709\u660E\u663E\u4E0D\u9002\uFF0C\u8BF7\u53CA\u65F6\u8054\u7CFB\u533B\u52A1\u4EBA\u5458\u3002",
  576. highlights: [
  577. { name: "\u767D\u7EC6\u80DE\u8BA1\u6570", value: "11.2", flag: "\u504F\u9AD8" },
  578. { name: "\u4E2D\u6027\u7C92\u7EC6\u80DE\u6BD4\u4F8B", value: "78%", flag: "\u504F\u9AD8" },
  579. { name: "\u6DCB\u5DF4\u7EC6\u80DE\u6BD4\u4F8B", value: "18%", flag: "\u504F\u4F4E" }
  580. ],
  581. explanations: [
  582. {
  583. indicator: "\u767D\u7EC6\u80DE 11.2 \xD710\u2079/L",
  584. explanation: "\u8F7B\u5EA6\u9AD8\u4E8E\u62A5\u544A\u6240\u793A\u53C2\u8003\u8303\u56F4\uFF0C\u5355\u9879\u53D8\u5316\u4E0D\u80FD\u72EC\u7ACB\u8BF4\u660E\u5177\u4F53\u539F\u56E0\u3002",
  585. commonFactors: "\u8FD1\u671F\u611F\u67D3\u3001\u7761\u7720\u4E0D\u8DB3\u3001\u8FD0\u52A8\u6216\u60C5\u7EEA\u7D27\u5F20\u7B49\u3002"
  586. },
  587. {
  588. indicator: "\u4E2D\u6027\u7C92\u7EC6\u80DE\u6BD4\u4F8B 78%",
  589. explanation: "\u6BD4\u4F8B\u8F7B\u5EA6\u5347\u9AD8\uFF0C\u9700\u7ED3\u5408\u767D\u7EC6\u80DE\u603B\u6570\u548C\u5F53\u524D\u75C7\u72B6\u5224\u65AD\u3002",
  590. commonFactors: "\u7EC6\u83CC\u611F\u67D3\u3001\u6025\u6027\u708E\u75C7\u6216\u751F\u7406\u5E94\u6FC0\u7B49\u3002"
  591. },
  592. {
  593. indicator: "\u6DCB\u5DF4\u7EC6\u80DE\u6BD4\u4F8B 18%",
  594. explanation: "\u6BD4\u4F8B\u8F7B\u5EA6\u504F\u4F4E\uFF0C\u53EF\u80FD\u4E0E\u4E2D\u6027\u7C92\u7EC6\u80DE\u6BD4\u4F8B\u5347\u9AD8\u540C\u65F6\u51FA\u73B0\u3002",
  595. commonFactors: "\u611F\u67D3\u6062\u590D\u671F\u3001\u538B\u529B\u6216\u4E2A\u4F53\u77ED\u671F\u6CE2\u52A8\u7B49\u3002"
  596. }
  597. ],
  598. canDo: [
  599. "\u67E5\u770B\u5E76\u4FDD\u5B58\u533B\u9662\u539F\u59CB\u62A5\u544A\u3002",
  600. "\u7ED3\u5408\u8FD1\u671F\u75C7\u72B6\u548C\u65E2\u5F80\u60C5\u51B5\uFF0C\u5411\u5F00\u5355\u533B\u751F\u54A8\u8BE2\u3002",
  601. "\u5982\u6709\u660E\u663E\u4E0D\u9002\uFF0C\u53CA\u65F6\u8054\u7CFB\u533B\u52A1\u4EBA\u5458\u3002"
  602. ],
  603. evidence: [
  604. "\u533B\u9662\u539F\u59CB\u62A5\u544A\u4E2D\u7684\u6307\u6807\u7ED3\u679C\u3001\u5355\u4F4D\u3001\u53C2\u8003\u8303\u56F4\u548C\u5F02\u5E38\u6807\u8BB0\u3002",
  605. "\u533B\u9662\u5DF2\u5BA1\u6838\u7684\u7EC8\u7248\u62A5\u544A\u3002"
  606. ]
  607. }
  608. });
  609. var reportConsentId = (reportId) => `consent-ai-${reportId}`;
  610. var encodeReportId = (reportId) => String(reportId).replace(
  611. /[^A-Za-z0-9._-]/g,
  612. (character) => `~${character.charCodeAt(0).toString(16).padStart(4, "0")}`
  613. );
  614. var reportResultSnapshotId = (reportId) => `result-report-interpretation-${encodeReportId(reportId)}`;
  615. var reportFeedbackCategories = /* @__PURE__ */ new Set([
  616. "HELPFUL",
  617. "METRIC_INACCURATE",
  618. "HARD_TO_UNDERSTAND",
  619. "DIFFERS_FROM_DOCTOR",
  620. "OTHER"
  621. ]);
  622. var readReportIssuedFixture = (fixture) => {
  623. if (fixture.payload !== void 0 && !isRecord(fixture.payload)) {
  624. return null;
  625. }
  626. const payload = fixture.payload ?? {};
  627. const isExamination = payload.family === "EXAM";
  628. const report = {
  629. ...bloodRoutineReportFixture,
  630. ...payload,
  631. interpretationStatus: "NOT_REQUESTED",
  632. metrics: Array.isArray(payload.metrics) ? payload.metrics : isExamination ? [] : bloodRoutineReportFixture.metrics
  633. };
  634. if (typeof report.reportId !== "string" || !reportIdPattern.test(report.reportId) || !Number.isInteger(report.sourceVersion) || report.sourceVersion <= 0 || typeof report.sourceHash !== "string" || !reportHashPattern.test(report.sourceHash) || !reportSourceStatuses.has(report.sourceStatus) || !reportFamilies.has(report.family) || !reportTypes.has(report.reportType) || !reportVerificationStatuses.has(report.verificationStatus) || report.family === "LAB" && report.reportType !== "BLOOD_ROUTINE" || report.family === "EXAM" && report.reportType !== "IMAGING") {
  635. return null;
  636. }
  637. return deepFreeze(structuredClone(report));
  638. };
  639. var hasValidReportConsent = (state, reportId) => {
  640. if (typeof reportId !== "string" || reportId.length === 0 || !isRecord(state?.consents) || !isRecord(state?.consentScopeBindings)) {
  641. return false;
  642. }
  643. const consentId = reportConsentId(reportId);
  644. const consent = Object.hasOwn(state.consents, consentId) ? state.consents[consentId] : null;
  645. const binding = Object.hasOwn(state.consentScopeBindings, consentId) ? state.consentScopeBindings[consentId] : null;
  646. const expiresAt = Date.parse(consent?.expiresAt);
  647. const evaluatedAt = Date.parse(state?.demoNow);
  648. return consent?.id === consentId && consent.status === "GRANTED" && Number.isFinite(expiresAt) && Number.isFinite(evaluatedAt) && expiresAt > evaluatedAt && binding?.patientId === state?.activePatientId && binding?.reportId === reportId;
  649. };
  650. var selectValidReportResult = (state, reportId) => {
  651. if (typeof reportId !== "string" || reportId.length === 0) {
  652. return null;
  653. }
  654. const resultRef = reportResultSnapshotId(reportId);
  655. const reportSources = state?.reportSourceProjections;
  656. const results = state?.resultSnapshotProjections;
  657. const report = isRecord(reportSources) && Object.hasOwn(reportSources, reportId) ? reportSources[reportId] : null;
  658. const result = isRecord(results) && Object.hasOwn(results, resultRef) ? results[resultRef] : null;
  659. const hasScopedTask = Array.isArray(state?.patientWorkItems) && state.patientWorkItems.some((item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId);
  660. return isRecord(report) && report.reportId === reportId && report.interpretationStatus === "READY" && hasScopedTask && hasValidReportConsent(state, reportId) && evaluateReportInterpretationGate(state, reportId).allowed && isRecord(result) && result.resultRef === resultRef && result.reportId === reportId && result.sourceVersion === report.sourceVersion && result.sourceHash === report.sourceHash && Number.isInteger(result.resultVersion) && result.resultVersion > 0 && result.valid === true && isRecord(result.interpretation) ? result : null;
  661. };
  662. function selectHomeTasks(state) {
  663. const patientWorkItems = Array.isArray(state?.patientWorkItems) ? state.patientWorkItems.filter(
  664. (item) => item !== null && typeof item === "object" && !Array.isArray(item)
  665. ) : [];
  666. const priorityOf = ({ priority }) => Number.isFinite(priority) ? priority : 0;
  667. const rankOf = ({ status }) => typeof status === "string" && Object.hasOwn(statusRank, status) ? statusRank[status] : 4;
  668. return patientWorkItems.sort((left, right) => {
  669. const statusDelta = rankOf(left) - rankOf(right);
  670. return statusDelta || priorityOf(right) - priorityOf(left);
  671. });
  672. }
  673. function selectReports(state, filters = {}) {
  674. const family = filters?.family ?? "ALL";
  675. const status = filters?.status ?? "ALL";
  676. const projections = state?.reportSourceProjections;
  677. if (!projections || typeof projections !== "object" || Array.isArray(projections)) {
  678. return [];
  679. }
  680. return Object.values(projections).filter((report) => report && typeof report === "object" && !Array.isArray(report)).filter((report) => family === "ALL" || report.family === family).filter((report) => status === "ALL" || report.readStatus === status).sort((left, right) => {
  681. const issuedAtDelta = String(right.issuedAt ?? "").localeCompare(String(left.issuedAt ?? ""));
  682. return issuedAtDelta || String(left.reportId ?? "").localeCompare(String(right.reportId ?? ""));
  683. });
  684. }
  685. var upsertProjection = (items, nextItem) => {
  686. const source = Array.isArray(items) ? items : [];
  687. const index = source.findIndex((item) => item?.id === nextItem.id);
  688. if (index < 0) {
  689. return [...source, nextItem];
  690. }
  691. return source.map((item, itemIndex) => itemIndex === index ? { ...item, ...nextItem } : item);
  692. };
  693. var journeyTitleByStage = {
  694. WAITING_ARRIVAL: "\u5F85\u5230\u9662",
  695. CHECKIN_AVAILABLE: "\u53EF\u4EE5\u7B7E\u5230",
  696. QUEUE_WAITING: "\u5019\u8BCA\u4E2D",
  697. VISIT_IN_PROGRESS: "\u5C31\u8BCA\u4E2D",
  698. POST_VISIT: "\u8BCA\u540E\u670D\u52A1"
  699. };
  700. var updateJourneyProjection = (journeys, appointmentId, stage, details = {}) => upsertProjection(journeys, {
  701. id: `journey-${appointmentId}`,
  702. appointmentId,
  703. stage,
  704. title: journeyTitleByStage[stage],
  705. ...details
  706. });
  707. var appendJourneyMessage = (messages, fixture, text) => {
  708. if (!text) {
  709. return Array.isArray(messages) ? messages : [];
  710. }
  711. return [
  712. ...Array.isArray(messages) ? messages : [],
  713. {
  714. id: `message-${fixture.id}`,
  715. type: "JOURNEY",
  716. unread: true,
  717. title: text,
  718. createdAt: fixture.occurredAt
  719. }
  720. ];
  721. };
  722. var readReportInvalidationFixture = (fixture) => {
  723. const payload = isRecord(fixture?.payload) ? fixture.payload : null;
  724. const reportId = payload?.reportId;
  725. const sourceVersion = payload?.sourceVersion;
  726. const sourceHash = payload?.sourceHash;
  727. if (typeof reportId !== "string" || reportId.trim().length === 0 || !/^[A-Za-z0-9._-]+$/.test(reportId) || !Number.isInteger(sourceVersion) || sourceVersion <= 0 || typeof sourceHash !== "string" || sourceHash.trim().length === 0 || !/^[A-Za-z0-9._:-]+$/.test(sourceHash)) {
  728. return null;
  729. }
  730. return {
  731. reportId,
  732. sourceVersion,
  733. sourceHash: sourceHash.trim()
  734. };
  735. };
  736. var applyReportInvalidationProjection = (state, nextState, fixture) => {
  737. const { reportId, sourceVersion, sourceHash } = readReportInvalidationFixture(fixture);
  738. const report = typeof reportId === "string" && isRecord(state.reportSourceProjections) && Object.hasOwn(state.reportSourceProjections, reportId) ? state.reportSourceProjections[reportId] : null;
  739. const latestKnownVersion = Math.max(
  740. Number.isInteger(report?.sourceVersion) ? report.sourceVersion : 0,
  741. Number.isInteger(report?.invalidationVersion) ? report.invalidationVersion : 0
  742. );
  743. if (!isRecord(report) || report.reportId !== reportId || sourceVersion <= latestKnownVersion) {
  744. return nextState;
  745. }
  746. const withdrawn = fixture.type === "REPORT_WITHDRAWN";
  747. const resultSnapshotProjections = Object.fromEntries(
  748. Object.entries(state.resultSnapshotProjections ?? {}).map(
  749. ([resultRef, result]) => isRecord(result) && result.reportId === reportId ? [resultRef, {
  750. ...result,
  751. valid: false,
  752. invalidatedBy: fixture.id,
  753. invalidatedReason: withdrawn ? "REPORT_WITHDRAWN" : "REPORT_CORRECTED",
  754. invalidatedSourceVersion: sourceVersion
  755. }] : [resultRef, result]
  756. )
  757. );
  758. const reportSourceProjections = {
  759. ...state.reportSourceProjections,
  760. [reportId]: {
  761. ...report,
  762. interpretationStatus: "INVALIDATED",
  763. invalidationVersion: sourceVersion,
  764. invalidationHash: sourceHash,
  765. invalidatedAt: fixture.occurredAt,
  766. invalidationReason: withdrawn ? "REPORT_WITHDRAWN" : "REPORT_CORRECTED"
  767. }
  768. };
  769. const patientWorkItems = Array.isArray(state.patientWorkItems) ? state.patientWorkItems.map((item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId ? {
  770. ...item,
  771. status: "ACTION_REQUIRED",
  772. stage: "REPORT_INVALIDATED",
  773. title: withdrawn ? "\u62A5\u544A\u5DF2\u64A4\u56DE" : "\u62A5\u544A\u5DF2\u66F4\u6B63",
  774. description: withdrawn ? "\u533B\u9662\u5DF2\u64A4\u56DE\u5F53\u524D\u62A5\u544A\uFF0C\u8BF7\u4EE5\u533B\u9662\u540E\u7EED\u901A\u77E5\u4E3A\u51C6" : "\u533B\u9662\u5DF2\u66F4\u65B0\u62A5\u544A\uFF0C\u539F\u8F85\u52A9\u89E3\u91CA\u5DF2\u5931\u6548",
  775. primaryAction: "VIEW_REPORT",
  776. secondaryAction: null
  777. } : item) : state.patientWorkItems;
  778. const isActiveReport = state.overlays?.reportId === reportId;
  779. return {
  780. ...nextState,
  781. reportSourceProjections,
  782. patientWorkItems,
  783. resultSnapshotProjections,
  784. overlays: isActiveReport ? {
  785. ...state.overlays,
  786. consentId: null,
  787. consentScope: null,
  788. reportView: "REPORT_UNAVAILABLE",
  789. reportUnavailableReason: "REPORT_INVALIDATED",
  790. reportFeedbackOpen: false,
  791. reportFeedbackSubmitted: false,
  792. reportServiceNotice: null
  793. } : state.overlays
  794. };
  795. };
  796. function applyServerProjectionFixture(state, fixture) {
  797. if (!isRecord(state) || !Array.isArray(state.appliedProjectionFixtureIds) || !isRecord(fixture) || typeof fixture.id !== "string" || fixture.id.trim().length === 0 || typeof fixture.type !== "string" || fixture.type.trim().length === 0) {
  798. return state;
  799. }
  800. if (state.appliedProjectionFixtureIds.includes(fixture.id)) {
  801. return state;
  802. }
  803. const isReportInvalidation = fixture.type === "REPORT_CORRECTED" || fixture.type === "REPORT_WITHDRAWN";
  804. if (isReportInvalidation && !readReportInvalidationFixture(fixture)) {
  805. return state;
  806. }
  807. const issuedReport = fixture.type === "REPORT_ISSUED" ? readReportIssuedFixture(fixture) : null;
  808. if (fixture.type === "REPORT_ISSUED" && !issuedReport) {
  809. return state;
  810. }
  811. const nextState = {
  812. ...state,
  813. appliedProjectionFixtureIds: [
  814. ...state.appliedProjectionFixtureIds,
  815. fixture.id
  816. ]
  817. };
  818. if (fixture.type === "REPORT_ISSUED") {
  819. const report = issuedReport;
  820. const isExamination = report.family === "EXAM";
  821. const reportId = report.reportId;
  822. const currentReport = state.reportSourceProjections?.[reportId];
  823. const currentVersion = currentReport?.sourceVersion;
  824. const invalidationVersion = currentReport?.invalidationVersion;
  825. const conflictsWithInvalidation = Number.isInteger(invalidationVersion) && (report.sourceVersion < invalidationVersion || report.sourceVersion === invalidationVersion && report.sourceHash !== currentReport.invalidationHash);
  826. if (currentReport && (!Number.isInteger(report.sourceVersion) || !Number.isInteger(currentVersion) || report.sourceVersion <= currentVersion || conflictsWithInvalidation)) {
  827. return nextState;
  828. }
  829. const resultRef = reportResultSnapshotId(reportId);
  830. const currentResult = isRecord(state.resultSnapshotProjections) && Object.hasOwn(state.resultSnapshotProjections, resultRef) ? state.resultSnapshotProjections[resultRef] : null;
  831. const resultSnapshotProjections = currentReport && isRecord(currentResult) ? {
  832. ...state.resultSnapshotProjections,
  833. [resultRef]: {
  834. ...currentResult,
  835. valid: false,
  836. invalidatedReason: "SOURCE_UPDATED",
  837. invalidatedAt: fixture.occurredAt ?? state.demoNow
  838. }
  839. } : state.resultSnapshotProjections;
  840. const consentId = reportConsentId(reportId);
  841. const currentConsent = isRecord(state.consents) && Object.hasOwn(state.consents, consentId) ? state.consents[consentId] : null;
  842. const consents = currentReport && isRecord(currentConsent) ? {
  843. ...state.consents,
  844. [consentId]: {
  845. ...currentConsent,
  846. status: "REVOKED",
  847. revokedAt: fixture.occurredAt ?? state.demoNow
  848. }
  849. } : state.consents;
  850. const consentScopeBindings = isRecord(state.consentScopeBindings) ? Object.fromEntries(
  851. Object.entries(state.consentScopeBindings).filter(
  852. ([bindingId]) => !currentReport || bindingId !== consentId
  853. )
  854. ) : state.consentScopeBindings;
  855. return {
  856. ...nextState,
  857. reportSourceProjections: {
  858. ...state.reportSourceProjections ?? {},
  859. [reportId]: report
  860. },
  861. resultSnapshotProjections,
  862. consents,
  863. consentScopeBindings,
  864. overlays: currentReport && state.overlays?.reportId === reportId ? {
  865. ...state.overlays,
  866. consentId: null,
  867. consentScope: null,
  868. reportView: "REPORT_DETAIL",
  869. reportUnavailableReason: null,
  870. reportFeedbackOpen: false,
  871. reportFeedbackSubmitted: false,
  872. reportServiceNotice: null
  873. } : state.overlays,
  874. patientWorkItems: upsertProjection(
  875. state.patientWorkItems,
  876. {
  877. id: `work-item-${reportId}`,
  878. reportId,
  879. type: "REPORT_REVIEW",
  880. status: "ACTION_REQUIRED",
  881. stage: "REPORT_AVAILABLE",
  882. priority: 90,
  883. title: `${typeof report.title === "string" && report.title.trim() ? report.title.trim() : isExamination ? "\u68C0\u67E5" : "\u68C0\u9A8C"}\u62A5\u544A\u5DF2\u51FA`,
  884. description: isExamination ? "\u533B\u9662\u68C0\u67E5\u62A5\u544A\u5DF2\u5BA1\u6838\uFF0C\u53EF\u67E5\u770B\u62A5\u544A\u8BE6\u60C5" : "18\u9879\u6307\u6807\u4E2D\u67093\u9879\u8D85\u51FA\u53C2\u8003\u8303\u56F4",
  885. primaryAction: isExamination ? "VIEW_REPORT" : "INTERPRET_REPORT",
  886. secondaryAction: isExamination ? null : "VIEW_REPORT",
  887. expiresAt: null
  888. }
  889. ),
  890. messages: upsertProjection(
  891. state.messages,
  892. {
  893. id: `message-${reportId}`,
  894. type: "REPORT",
  895. reportId,
  896. unread: true,
  897. title: `${typeof report.title === "string" && report.title.trim() ? report.title.trim() : isExamination ? "\u68C0\u67E5" : "\u68C0\u9A8C"}\u62A5\u544A\u5DF2\u51FA`,
  898. createdAt: fixture.occurredAt
  899. }
  900. )
  901. };
  902. }
  903. if (isReportInvalidation) {
  904. return applyReportInvalidationProjection(state, nextState, fixture);
  905. }
  906. const payload = fixture.payload ?? {};
  907. const appointmentId = payload.appointmentId ?? "appointment-001";
  908. let patientWorkItems = state.patientWorkItems;
  909. let journeyProjections = state.journeyProjections;
  910. let messages = state.messages;
  911. switch (fixture.type) {
  912. case "APPOINTMENT_CREATED": {
  913. patientWorkItems = upsertProjection(patientWorkItems, {
  914. id: `work-item-${appointmentId}`,
  915. appointmentId,
  916. type: "APPOINTMENT",
  917. status: "UPCOMING",
  918. stage: "WAITING_ARRIVAL",
  919. priority: 80,
  920. title: `${payload.departmentName ?? "\u95E8\u8BCA"}\u9884\u7EA6\u6210\u529F`,
  921. description: `${payload.visitTime ?? "09:30"} \xB7 ${payload.doctorName ?? "\u63A5\u8BCA\u533B\u751F"}`,
  922. primaryAction: null,
  923. secondaryAction: null,
  924. expiresAt: null
  925. });
  926. journeyProjections = updateJourneyProjection(
  927. journeyProjections,
  928. appointmentId,
  929. "WAITING_ARRIVAL",
  930. {
  931. visitTime: payload.visitTime ?? "09:30",
  932. departmentName: payload.departmentName ?? "\u795E\u7ECF\u5185\u79D1",
  933. doctorName: payload.doctorName ?? "\u674E\u660E"
  934. }
  935. );
  936. messages = appendJourneyMessage(messages, fixture, "\u9884\u7EA6\u6210\u529F\uFF0C\u8BF7\u6309\u65F6\u5230\u9662");
  937. break;
  938. }
  939. case "CHECKIN_AVAILABLE": {
  940. patientWorkItems = upsertProjection(patientWorkItems, {
  941. id: `work-item-checkin-${appointmentId}`,
  942. appointmentId,
  943. type: "CHECK_IN",
  944. status: "ACTION_REQUIRED",
  945. stage: "CONFIRMATION_REQUIRED",
  946. priority: 95,
  947. title: "\u795E\u7ECF\u5185\u79D1\u53EF\u4EE5\u7B7E\u5230",
  948. description: `\u5DF2\u5230\u8FBE${payload.location ?? "\u95E8\u8BCA\u8BCA\u533A"}\uFF0C\u8BF7\u786E\u8BA4\u540E\u7B7E\u5230`,
  949. location: payload.location,
  950. primaryAction: "CONFIRM_CHECKIN",
  951. secondaryAction: "VIEW_ROUTE",
  952. expiresAt: null
  953. });
  954. journeyProjections = updateJourneyProjection(
  955. journeyProjections,
  956. appointmentId,
  957. "CHECKIN_AVAILABLE",
  958. { location: payload.location, arrivedAt: fixture.occurredAt }
  959. );
  960. messages = appendJourneyMessage(messages, fixture, "\u5DF2\u5230\u8FBE\u8BCA\u533A\uFF0C\u53EF\u4EE5\u7B7E\u5230");
  961. break;
  962. }
  963. case "CHECKIN_COMPLETED": {
  964. patientWorkItems = upsertProjection(patientWorkItems, {
  965. id: `work-item-checkin-${appointmentId}`,
  966. appointmentId,
  967. type: "CHECK_IN",
  968. status: "COMPLETED",
  969. stage: "COMPLETED",
  970. priority: 95,
  971. title: "\u7B7E\u5230\u5DF2\u5B8C\u6210",
  972. description: "\u5DF2\u8FDB\u5165\u5019\u8BCA\u961F\u5217",
  973. primaryAction: null,
  974. secondaryAction: null,
  975. expiresAt: null
  976. });
  977. patientWorkItems = upsertProjection(patientWorkItems, {
  978. id: `work-item-queue-${appointmentId}`,
  979. appointmentId,
  980. type: "WAITING_QUEUE",
  981. status: "IN_PROGRESS",
  982. stage: "QUEUE_WAITING",
  983. priority: 90,
  984. title: "\u795E\u7ECF\u5185\u79D1\u5019\u8BCA\u4E2D",
  985. description: `\u6392\u961F\u53F7 ${payload.queueNumber ?? "\u5F85\u66F4\u65B0"}`,
  986. queueNumber: payload.queueNumber,
  987. room: payload.room,
  988. primaryAction: "VIEW_QUEUE",
  989. secondaryAction: "VIEW_ROUTE",
  990. expiresAt: null
  991. });
  992. journeyProjections = updateJourneyProjection(
  993. journeyProjections,
  994. appointmentId,
  995. "QUEUE_WAITING",
  996. {
  997. checkedInAt: fixture.occurredAt,
  998. queueNumber: payload.queueNumber,
  999. room: payload.room
  1000. }
  1001. );
  1002. messages = appendJourneyMessage(messages, fixture, "\u7B7E\u5230\u6210\u529F\uFF0C\u5DF2\u8FDB\u5165\u5019\u8BCA\u961F\u5217");
  1003. break;
  1004. }
  1005. case "QUEUE_UPDATED": {
  1006. const currentQueue = patientWorkItems.find(
  1007. (item) => item?.id === `work-item-queue-${appointmentId}`
  1008. );
  1009. patientWorkItems = upsertProjection(patientWorkItems, {
  1010. ...currentQueue ?? {},
  1011. id: `work-item-queue-${appointmentId}`,
  1012. appointmentId,
  1013. type: "WAITING_QUEUE",
  1014. status: "IN_PROGRESS",
  1015. stage: "QUEUE_WAITING",
  1016. priority: 90,
  1017. title: "\u795E\u7ECF\u5185\u79D1\u5019\u8BCA\u4E2D",
  1018. description: `\u524D\u65B9 ${payload.peopleAhead ?? 0} \u4EBA\uFF0C\u9884\u8BA1 ${payload.estimatedMinutes ?? 0} \u5206\u949F`,
  1019. queueNumber: payload.queueNumber ?? currentQueue?.queueNumber,
  1020. peopleAhead: payload.peopleAhead,
  1021. estimatedMinutes: payload.estimatedMinutes,
  1022. room: payload.room ?? currentQueue?.room,
  1023. delayed: Boolean(payload.delayed),
  1024. updatedAt: fixture.occurredAt,
  1025. primaryAction: "VIEW_QUEUE",
  1026. secondaryAction: "VIEW_ROUTE",
  1027. expiresAt: null
  1028. });
  1029. journeyProjections = updateJourneyProjection(
  1030. journeyProjections,
  1031. appointmentId,
  1032. "QUEUE_WAITING",
  1033. {
  1034. queueNumber: payload.queueNumber,
  1035. peopleAhead: payload.peopleAhead,
  1036. estimatedMinutes: payload.estimatedMinutes,
  1037. room: payload.room,
  1038. delayed: Boolean(payload.delayed),
  1039. updatedAt: fixture.occurredAt
  1040. }
  1041. );
  1042. messages = appendJourneyMessage(messages, fixture, "\u5019\u8BCA\u961F\u5217\u5DF2\u66F4\u65B0");
  1043. break;
  1044. }
  1045. case "VISIT_STARTED": {
  1046. patientWorkItems = upsertProjection(patientWorkItems, {
  1047. id: `work-item-queue-${appointmentId}`,
  1048. appointmentId,
  1049. type: "WAITING_QUEUE",
  1050. status: "COMPLETED",
  1051. stage: "COMPLETED",
  1052. priority: 90,
  1053. title: "\u5019\u8BCA\u5DF2\u7ED3\u675F",
  1054. description: "\u533B\u751F\u5DF2\u5F00\u59CB\u63A5\u8BCA",
  1055. primaryAction: null,
  1056. secondaryAction: null,
  1057. expiresAt: null
  1058. });
  1059. patientWorkItems = upsertProjection(patientWorkItems, {
  1060. id: `work-item-visit-${appointmentId}`,
  1061. appointmentId,
  1062. type: "VISIT",
  1063. status: "IN_PROGRESS",
  1064. stage: "VISIT_IN_PROGRESS",
  1065. priority: 100,
  1066. title: "\u533B\u751F\u63A5\u8BCA\u4E2D",
  1067. description: payload.room ?? "\u8BF7\u5728\u8BCA\u5BA4\u5185\u5B8C\u6210\u5C31\u8BCA",
  1068. primaryAction: null,
  1069. secondaryAction: null,
  1070. expiresAt: null
  1071. });
  1072. journeyProjections = updateJourneyProjection(
  1073. journeyProjections,
  1074. appointmentId,
  1075. "VISIT_IN_PROGRESS",
  1076. { room: payload.room, startedAt: fixture.occurredAt }
  1077. );
  1078. messages = appendJourneyMessage(messages, fixture, "\u533B\u751F\u5DF2\u5F00\u59CB\u63A5\u8BCA");
  1079. break;
  1080. }
  1081. case "VISIT_COMPLETED": {
  1082. patientWorkItems = upsertProjection(patientWorkItems, {
  1083. id: `work-item-visit-${appointmentId}`,
  1084. appointmentId,
  1085. type: "VISIT",
  1086. status: "COMPLETED",
  1087. stage: "COMPLETED",
  1088. priority: 100,
  1089. title: "\u672C\u6B21\u5C31\u8BCA\u5DF2\u5B8C\u6210",
  1090. description: "\u8BCA\u540E\u8BB0\u5F55\u4E0E\u670D\u52A1\u5DF2\u66F4\u65B0",
  1091. primaryAction: null,
  1092. secondaryAction: null,
  1093. expiresAt: null
  1094. });
  1095. patientWorkItems = upsertProjection(patientWorkItems, {
  1096. id: `work-item-post-visit-${appointmentId}`,
  1097. appointmentId,
  1098. type: "POST_VISIT",
  1099. status: "ACTION_REQUIRED",
  1100. stage: "POST_VISIT",
  1101. priority: 70,
  1102. title: "\u67E5\u770B\u8BCA\u540E\u670D\u52A1",
  1103. description: "\u67E5\u770B\u95E8\u8BCA\u8BB0\u5F55\u3001\u5904\u65B9\u548C\u590D\u8BCA\u5EFA\u8BAE",
  1104. primaryAction: null,
  1105. secondaryAction: null,
  1106. expiresAt: null
  1107. });
  1108. journeyProjections = updateJourneyProjection(
  1109. journeyProjections,
  1110. appointmentId,
  1111. "POST_VISIT",
  1112. { completedAt: fixture.occurredAt }
  1113. );
  1114. messages = appendJourneyMessage(messages, fixture, "\u5C31\u8BCA\u5B8C\u6210\uFF0C\u8BCA\u540E\u670D\u52A1\u5DF2\u66F4\u65B0");
  1115. break;
  1116. }
  1117. default:
  1118. return nextState;
  1119. }
  1120. return {
  1121. ...nextState,
  1122. patientWorkItems,
  1123. journeyProjections,
  1124. messages,
  1125. activeAgentTaskProjection: state.activeAgentTaskProjection,
  1126. commandProjections: state.commandProjections
  1127. };
  1128. }
  1129. function requestReportInterpretation(state, reportId) {
  1130. if (!Array.isArray(state?.patientWorkItems)) {
  1131. return state;
  1132. }
  1133. const itemIndex = state.patientWorkItems.findIndex(
  1134. (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId
  1135. ) ?? -1;
  1136. if (itemIndex < 0) {
  1137. return state;
  1138. }
  1139. const hasConsent = hasValidReportConsent(state, reportId);
  1140. const gate = evaluateReportInterpretationGate(state, reportId);
  1141. const consentId = reportConsentId(reportId);
  1142. if (!gate.allowed) {
  1143. const unavailableReason = [
  1144. "OCR_REVIEW_REQUIRED",
  1145. "CRITICAL_NOTICE",
  1146. "AI_UNAVAILABLE",
  1147. "UNSUPPORTED"
  1148. ].includes(gate.reason) ? gate.reason : "UNSUPPORTED";
  1149. const currentInterpretationStatus = state.reportSourceProjections?.[reportId]?.interpretationStatus;
  1150. return {
  1151. ...state,
  1152. patientWorkItems: state.patientWorkItems.map((item, index) => index === itemIndex ? { ...item, stage: "REPORT_UNAVAILABLE" } : item),
  1153. reportSourceProjections: {
  1154. ...state.reportSourceProjections,
  1155. [reportId]: {
  1156. ...state.reportSourceProjections[reportId],
  1157. interpretationStatus: currentInterpretationStatus === "INVALIDATED" ? "INVALIDATED" : "NOT_REQUESTED"
  1158. }
  1159. },
  1160. overlays: {
  1161. ...state.overlays,
  1162. consentId: null,
  1163. consentScope: null,
  1164. reportId,
  1165. reportView: "REPORT_UNAVAILABLE",
  1166. reportUnavailableReason: unavailableReason
  1167. }
  1168. };
  1169. }
  1170. const existingResult = selectValidReportResult(state, reportId);
  1171. if (hasConsent && gate.allowed && state.reportSourceProjections?.[reportId]?.interpretationStatus === "READY" && existingResult) {
  1172. return {
  1173. ...state,
  1174. patientWorkItems: state.patientWorkItems.map((item, index) => index === itemIndex ? { ...item, stage: "INTERPRETED" } : item),
  1175. overlays: {
  1176. ...state.overlays,
  1177. consentId: null,
  1178. consentScope: null,
  1179. reportId,
  1180. reportView: "INTERPRETED"
  1181. }
  1182. };
  1183. }
  1184. const patientWorkItems = state.patientWorkItems.map((item, index) => index === itemIndex ? { ...item, stage: hasConsent ? "INTERPRETING" : "CONSENT_REQUIRED" } : item);
  1185. return {
  1186. ...state,
  1187. patientWorkItems,
  1188. reportSourceProjections: hasConsent ? {
  1189. ...state.reportSourceProjections,
  1190. [reportId]: {
  1191. ...state.reportSourceProjections[reportId],
  1192. interpretationStatus: "GENERATING"
  1193. }
  1194. } : state.reportSourceProjections,
  1195. overlays: {
  1196. ...state.overlays,
  1197. consentId: hasConsent ? null : consentId,
  1198. consentScope: hasConsent ? null : {
  1199. patientId: state.activePatientId,
  1200. reportId
  1201. },
  1202. reportId,
  1203. reportView: hasConsent ? "INTERPRETING" : state?.overlays?.reportView ?? null
  1204. }
  1205. };
  1206. }
  1207. function evaluateReportInterpretationGate(state, reportId) {
  1208. const report = state?.reportSourceProjections?.[reportId];
  1209. if (!report || typeof report !== "object" || Array.isArray(report)) {
  1210. return { allowed: false, reason: "REPORT_NOT_FOUND" };
  1211. }
  1212. if (report.family !== "LAB" || report.reportType !== "BLOOD_ROUTINE") {
  1213. return { allowed: false, reason: "UNSUPPORTED" };
  1214. }
  1215. if (report.sourceStatus !== "FINAL") {
  1216. return { allowed: false, reason: "SOURCE_NOT_FINAL" };
  1217. }
  1218. if (report.verificationStatus !== "VERIFIED") {
  1219. return {
  1220. allowed: false,
  1221. reason: typeof report.verificationReason === "string" ? report.verificationReason : "VERIFICATION_REQUIRED"
  1222. };
  1223. }
  1224. if (report.criticalNotice === true) {
  1225. return { allowed: false, reason: "CRITICAL_NOTICE" };
  1226. }
  1227. if (report.interpretationStatus === "INVALIDATED") {
  1228. return { allowed: false, reason: "REPORT_INVALIDATED" };
  1229. }
  1230. if (report.interpretationUnavailableReason === "AI_UNAVAILABLE" || report.interpretationUnavailableReason === "UNSUPPORTED") {
  1231. return {
  1232. allowed: false,
  1233. reason: report.interpretationUnavailableReason
  1234. };
  1235. }
  1236. return { allowed: true, reason: null };
  1237. }
  1238. function grantConsent(state, consentId) {
  1239. const reportId = state?.overlays?.reportId;
  1240. const expectedConsentId = typeof reportId === "string" ? reportConsentId(reportId) : null;
  1241. const scope = state?.overlays?.consentScope;
  1242. if (consentId !== expectedConsentId || state?.overlays?.consentId !== consentId || scope?.patientId !== state?.activePatientId || scope?.reportId !== reportId || !Array.isArray(state?.patientWorkItems)) {
  1243. return state;
  1244. }
  1245. const itemIndex = state?.patientWorkItems?.findIndex(
  1246. (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "CONSENT_REQUIRED"
  1247. ) ?? -1;
  1248. if (itemIndex < 0 || !evaluateReportInterpretationGate(state, reportId).allowed) {
  1249. return state;
  1250. }
  1251. const consent = {
  1252. id: consentId,
  1253. purpose: "\u7528\u4E8E\u672C\u6B21\u62A5\u544A\u7684 AI \u8F85\u52A9\u89E3\u91CA",
  1254. scope: "\u5F53\u524D\u8840\u5E38\u89C4\u62A5\u544A",
  1255. expiresAt: "2026-07-09T23:59:59+08:00",
  1256. revocable: true,
  1257. status: "GRANTED"
  1258. };
  1259. const binding = {
  1260. patientId: state.activePatientId,
  1261. reportId
  1262. };
  1263. const pendingConsentState = {
  1264. ...state,
  1265. consents: {
  1266. ...state.consents,
  1267. [consentId]: consent
  1268. },
  1269. consentScopeBindings: {
  1270. ...state.consentScopeBindings,
  1271. [consentId]: binding
  1272. }
  1273. };
  1274. if (!hasValidReportConsent(pendingConsentState, reportId)) {
  1275. return {
  1276. ...state,
  1277. patientWorkItems: state.patientWorkItems.map((item, index) => index === itemIndex ? { ...item, stage: "REPORT_AVAILABLE" } : item),
  1278. reportSourceProjections: {
  1279. ...state.reportSourceProjections,
  1280. [reportId]: {
  1281. ...state.reportSourceProjections[reportId],
  1282. interpretationStatus: "NOT_REQUESTED"
  1283. }
  1284. },
  1285. overlays: {
  1286. ...state.overlays,
  1287. consentId: null,
  1288. consentScope: null,
  1289. reportView: "REPORT_DETAIL"
  1290. }
  1291. };
  1292. }
  1293. return {
  1294. ...state,
  1295. consents: pendingConsentState.consents,
  1296. consentScopeBindings: pendingConsentState.consentScopeBindings,
  1297. reportSourceProjections: {
  1298. ...state.reportSourceProjections,
  1299. [reportId]: {
  1300. ...state.reportSourceProjections[reportId],
  1301. interpretationStatus: "GENERATING"
  1302. }
  1303. },
  1304. patientWorkItems: state.patientWorkItems.map((item, index) => {
  1305. if (index === itemIndex) {
  1306. return { ...item, stage: "INTERPRETING" };
  1307. }
  1308. return item;
  1309. }),
  1310. resultSnapshotProjections: {
  1311. ...state.resultSnapshotProjections ?? {}
  1312. },
  1313. overlays: {
  1314. ...state.overlays,
  1315. consentId: null,
  1316. consentScope: null,
  1317. reportView: "INTERPRETING"
  1318. }
  1319. };
  1320. }
  1321. function openReportDetails(state, reportId) {
  1322. const exists = state?.patientWorkItems?.some(
  1323. (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId
  1324. );
  1325. const report = state?.reportSourceProjections?.[reportId];
  1326. const hasConsistentSource = report && typeof report === "object" && !Array.isArray(report) && (report.reportId === void 0 || report.reportId === reportId);
  1327. if (!exists || !hasConsistentSource) {
  1328. return state;
  1329. }
  1330. const isExamination = report.family === "EXAM";
  1331. const gate = isExamination ? { allowed: true, reason: null } : evaluateReportInterpretationGate(state, reportId);
  1332. const unavailableReason = gate.allowed ? null : [
  1333. "OCR_REVIEW_REQUIRED",
  1334. "CRITICAL_NOTICE",
  1335. "AI_UNAVAILABLE",
  1336. "UNSUPPORTED"
  1337. ].includes(gate.reason) ? gate.reason : "UNSUPPORTED";
  1338. return {
  1339. ...state,
  1340. reportSourceProjections: {
  1341. ...state.reportSourceProjections ?? {},
  1342. [reportId]: {
  1343. ...report,
  1344. readStatus: "READ"
  1345. }
  1346. },
  1347. overlays: {
  1348. ...state.overlays,
  1349. reportId,
  1350. reportView: unavailableReason ? "REPORT_UNAVAILABLE" : "REPORT_DETAIL",
  1351. reportUnavailableReason: unavailableReason,
  1352. reportExpanded: false,
  1353. reportFeedbackOpen: false,
  1354. reportFeedbackSubmitted: false,
  1355. reportServiceNotice: null
  1356. }
  1357. };
  1358. }
  1359. function closeReportView(state) {
  1360. if (!state?.overlays?.reportId && !state?.overlays?.reportView) {
  1361. return state;
  1362. }
  1363. return {
  1364. ...state,
  1365. overlays: {
  1366. ...state.overlays,
  1367. consentId: null,
  1368. consentScope: null,
  1369. reportId: null,
  1370. reportView: null,
  1371. reportUnavailableReason: null,
  1372. reportExpanded: false,
  1373. reportFeedbackOpen: false,
  1374. reportFeedbackSubmitted: false,
  1375. reportServiceNotice: null
  1376. }
  1377. };
  1378. }
  1379. function rejectConsent(state, consentId) {
  1380. const reportId = state?.overlays?.reportId;
  1381. if (consentId !== reportConsentId(reportId) || state?.overlays?.consentId !== consentId || state?.overlays?.consentScope?.patientId !== state?.activePatientId || state?.overlays?.consentScope?.reportId !== reportId || !Array.isArray(state?.patientWorkItems)) {
  1382. return state;
  1383. }
  1384. const hasPendingItem = state.patientWorkItems?.some(
  1385. (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "CONSENT_REQUIRED"
  1386. );
  1387. if (typeof reportId !== "string" || !hasPendingItem) {
  1388. return state;
  1389. }
  1390. return {
  1391. ...state,
  1392. patientWorkItems: state.patientWorkItems.map((item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId ? { ...item, stage: "REPORT_AVAILABLE" } : item),
  1393. overlays: {
  1394. ...state.overlays,
  1395. consentId: null,
  1396. consentScope: null,
  1397. reportView: "REPORT_DETAIL"
  1398. }
  1399. };
  1400. }
  1401. function applyReportInterpretationProjectionFixture(state, reportId) {
  1402. const itemIndex = state?.patientWorkItems?.findIndex(
  1403. (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "INTERPRETING"
  1404. ) ?? -1;
  1405. const consentGranted = hasValidReportConsent(state, reportId);
  1406. const gate = evaluateReportInterpretationGate(state, reportId);
  1407. const source = state?.reportSourceProjections?.[reportId];
  1408. if (itemIndex < 0 || !consentGranted || !gate.allowed || !isRecord(source)) {
  1409. return state;
  1410. }
  1411. const result = {
  1412. ...reportInterpretationProjectionFixture,
  1413. resultRef: reportResultSnapshotId(reportId),
  1414. reportId,
  1415. sourceVersion: source.sourceVersion,
  1416. sourceHash: source.sourceHash,
  1417. valid: true,
  1418. interpretation: {
  1419. ...reportInterpretationProjectionFixture.interpretation,
  1420. highlights: reportInterpretationProjectionFixture.interpretation.highlights.map((highlight) => ({ ...highlight })),
  1421. explanations: reportInterpretationProjectionFixture.interpretation.explanations.map((explanation) => ({ ...explanation })),
  1422. canDo: [
  1423. ...reportInterpretationProjectionFixture.interpretation.canDo
  1424. ],
  1425. evidence: [
  1426. ...reportInterpretationProjectionFixture.interpretation.evidence
  1427. ]
  1428. }
  1429. };
  1430. return {
  1431. ...state,
  1432. patientWorkItems: state.patientWorkItems.map((item, index) => index === itemIndex ? { ...item, stage: "INTERPRETED" } : item),
  1433. reportSourceProjections: {
  1434. ...state.reportSourceProjections,
  1435. [reportId]: {
  1436. ...state.reportSourceProjections[reportId],
  1437. interpretationStatus: "READY"
  1438. }
  1439. },
  1440. resultSnapshotProjections: {
  1441. ...state.resultSnapshotProjections,
  1442. [result.resultRef]: result
  1443. },
  1444. overlays: {
  1445. ...state.overlays,
  1446. consentId: null,
  1447. reportId,
  1448. reportView: "INTERPRETED"
  1449. }
  1450. };
  1451. }
  1452. function submitReportFeedback(state, payload = {}) {
  1453. const reportId = payload?.reportId;
  1454. const category = payload?.category;
  1455. const reportSources = state?.reportSourceProjections;
  1456. const report = typeof reportId === "string" && reportId.length > 0 && reportSources && typeof reportSources === "object" && !Array.isArray(reportSources) && Object.hasOwn(reportSources, reportId) ? reportSources[reportId] : null;
  1457. const hasReportTask = Array.isArray(state?.patientWorkItems) && state.patientWorkItems.some((item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId);
  1458. const result = selectValidReportResult(state, reportId);
  1459. if (!report || typeof report !== "object" || Array.isArray(report) || report.reportId !== reportId || !hasReportTask || !reportFeedbackCategories.has(category) || !Array.isArray(state?.reportFeedback) || !hasValidReportConsent(state, reportId) || !evaluateReportInterpretationGate(state, reportId).allowed || report.interpretationStatus !== "READY" || !result) {
  1460. return state;
  1461. }
  1462. const feedback = {
  1463. id: [
  1464. "feedback",
  1465. encodeReportId(reportId),
  1466. report.sourceVersion,
  1467. category
  1468. ].join("-"),
  1469. reportId,
  1470. sourceVersion: report.sourceVersion,
  1471. sourceHash: report.sourceHash,
  1472. resultRef: result.resultRef,
  1473. resultVersion: result.resultVersion,
  1474. category
  1475. };
  1476. if (state.reportFeedback.some((item) => item?.id === feedback.id)) {
  1477. return state;
  1478. }
  1479. return {
  1480. ...state,
  1481. reportFeedback: [...state.reportFeedback, feedback]
  1482. };
  1483. }
  1484. function revokeConsent(state, consentId) {
  1485. if (!isRecord(state) || typeof consentId !== "string" || !isRecord(state.consents) || !isRecord(state.consentScopeBindings) || !isRecord(state.reportSourceProjections) || !isRecord(state.resultSnapshotProjections) || !isRecord(state.overlays) || !Array.isArray(state.patientWorkItems) || !Object.hasOwn(state.consents, consentId) || !Object.hasOwn(state.consentScopeBindings, consentId)) {
  1486. return state;
  1487. }
  1488. const consent = state.consents[consentId];
  1489. const binding = state.consentScopeBindings[consentId];
  1490. if (!isRecord(consent) || !isRecord(binding) || typeof binding.reportId !== "string" || !Object.hasOwn(state.reportSourceProjections, binding.reportId) || !isRecord(state.reportSourceProjections[binding.reportId]) || consentId !== reportConsentId(binding.reportId) || consent?.id !== consentId || binding?.patientId !== state?.activePatientId) {
  1491. return state;
  1492. }
  1493. const reportId = binding.reportId;
  1494. const consentScopeBindings = Object.fromEntries(
  1495. Object.entries(state.consentScopeBindings).filter(
  1496. ([bindingConsentId]) => bindingConsentId !== consentId
  1497. )
  1498. );
  1499. const resultSnapshotProjections = Object.fromEntries(
  1500. Object.entries(state.resultSnapshotProjections ?? {}).filter(
  1501. ([, result]) => result?.reportId !== reportId
  1502. )
  1503. );
  1504. return {
  1505. ...state,
  1506. consents: {
  1507. ...state.consents,
  1508. [consentId]: {
  1509. ...consent,
  1510. status: "REVOKED"
  1511. }
  1512. },
  1513. consentScopeBindings,
  1514. reportSourceProjections: {
  1515. ...state.reportSourceProjections,
  1516. [reportId]: {
  1517. ...state.reportSourceProjections[reportId],
  1518. interpretationStatus: "NOT_REQUESTED"
  1519. }
  1520. },
  1521. patientWorkItems: state.patientWorkItems.map((item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId ? { ...item, stage: "REPORT_AVAILABLE" } : item),
  1522. resultSnapshotProjections,
  1523. overlays: {
  1524. ...state.overlays,
  1525. consentId: null,
  1526. consentScope: null,
  1527. reportId,
  1528. reportView: "REPORT_DETAIL",
  1529. reportFeedbackOpen: false,
  1530. reportFeedbackSubmitted: false,
  1531. reportServiceNotice: null
  1532. }
  1533. };
  1534. }
  1535. var scenarioNames = /* @__PURE__ */ new Set([
  1536. "REPORT_ARRIVED",
  1537. "REPORT_DETAIL",
  1538. "REPORT_CONSENT",
  1539. "REPORT_GENERATING",
  1540. "REPORT_READY",
  1541. "REPORT_NEED_REVIEW",
  1542. "REPORT_CRITICAL",
  1543. "REPORT_INVALIDATED",
  1544. "EMPTY_HOME",
  1545. "REGISTRATION_QUERY",
  1546. "COMMAND_PREPARED",
  1547. "APPOINTMENT_CREATED",
  1548. "CHECKIN_AVAILABLE",
  1549. "QUEUE_UPDATED"
  1550. ]);
  1551. var loadRegistrationScenario = (base) => submitInteraction(
  1552. base,
  1553. registrationDemoText
  1554. );
  1555. var loadLockedRegistrationScenario = (base) => {
  1556. const queried = loadRegistrationScenario(base);
  1557. return applyCandidateSelectionResponseFixture(queried, {
  1558. id: "scenario-selection-success-001",
  1559. outcome: "SUCCESS",
  1560. resultRef: "result-slot-001",
  1561. resultVersion: 1,
  1562. candidateId: "candidate-slot-0930",
  1563. validation: {
  1564. tenantMatched: true,
  1565. patientMatched: true,
  1566. sessionMatched: true,
  1567. versionMatched: true,
  1568. notExpired: true,
  1569. realtimeAvailable: true,
  1570. l1LockSucceeded: true
  1571. },
  1572. lock: {
  1573. lockId: "lock-slot-001",
  1574. expiresAt: "2026-07-03T09:05:00+08:00"
  1575. }
  1576. });
  1577. };
  1578. var loadCommandPreparedScenario = (base) => {
  1579. const locked = loadLockedRegistrationScenario(base);
  1580. return applyCommandProjectionFixture(locked, {
  1581. id: "scenario-command-awaiting-001",
  1582. commandId: "command-register-001",
  1583. version: 1,
  1584. riskLevel: "L2",
  1585. status: "AWAITING_CONFIRMATION",
  1586. details: {
  1587. operationName: "\u786E\u8BA4\u6302\u53F7",
  1588. patientName: "\u5F20\u5148\u751F",
  1589. departmentName: "\u795E\u7ECF\u5185\u79D1",
  1590. doctorName: "\u674E\u660E",
  1591. doctorTitle: "\u4E3B\u4EFB\u533B\u5E08",
  1592. visitAt: "2026-07-03 09:30",
  1593. fee: "30\u5143",
  1594. expiresAt: "2026-07-03T09:05:00+08:00",
  1595. countdown: "\u5269\u4F59 5 \u5206\u949F",
  1596. candidateId: "candidate-slot-0930"
  1597. }
  1598. });
  1599. };
  1600. function loadScenario(state, scenarioName) {
  1601. const normalized = scenarioNames.has(scenarioName) ? scenarioName : "EMPTY_HOME";
  1602. const base = {
  1603. ...createInitialState({
  1604. activePatientId: state?.activePatientId ?? "patient-zhang-san"
  1605. }),
  1606. demoScenario: normalized
  1607. };
  1608. const reportFixture = {
  1609. id: "scenario-report-arrived-001",
  1610. type: "REPORT_ISSUED",
  1611. occurredAt: "2026-07-03T09:30:00+08:00",
  1612. payload: { reportId: "report-blood-20260702" }
  1613. };
  1614. switch (normalized) {
  1615. case "REPORT_ARRIVED":
  1616. return { ...applyServerProjectionFixture(base, reportFixture), demoScenario: normalized };
  1617. case "REPORT_DETAIL": {
  1618. const report = applyServerProjectionFixture(base, reportFixture);
  1619. return {
  1620. ...openReportDetails(report, "report-blood-20260702"),
  1621. demoScenario: normalized
  1622. };
  1623. }
  1624. case "REPORT_CONSENT": {
  1625. const report = openReportDetails(
  1626. applyServerProjectionFixture(base, reportFixture),
  1627. "report-blood-20260702"
  1628. );
  1629. return {
  1630. ...requestReportInterpretation(report, "report-blood-20260702"),
  1631. demoScenario: normalized
  1632. };
  1633. }
  1634. case "REPORT_GENERATING": {
  1635. const pending = loadScenario(base, "REPORT_CONSENT");
  1636. return {
  1637. ...grantConsent(
  1638. pending,
  1639. "consent-ai-report-blood-20260702"
  1640. ),
  1641. demoScenario: normalized
  1642. };
  1643. }
  1644. case "REPORT_NEED_REVIEW": {
  1645. const report = applyServerProjectionFixture(base, {
  1646. ...reportFixture,
  1647. payload: {
  1648. ...reportFixture.payload,
  1649. verificationStatus: "NEED_REVIEW",
  1650. verificationReason: "OCR_REVIEW_REQUIRED"
  1651. }
  1652. });
  1653. return {
  1654. ...openReportDetails(report, "report-blood-20260702"),
  1655. demoScenario: normalized
  1656. };
  1657. }
  1658. case "REPORT_CRITICAL": {
  1659. const report = applyServerProjectionFixture(base, {
  1660. ...reportFixture,
  1661. payload: {
  1662. ...reportFixture.payload,
  1663. criticalNotice: true
  1664. }
  1665. });
  1666. return {
  1667. ...openReportDetails(report, "report-blood-20260702"),
  1668. demoScenario: normalized
  1669. };
  1670. }
  1671. case "REPORT_INVALIDATED": {
  1672. const ready = loadScenario(base, "REPORT_READY");
  1673. return {
  1674. ...applyServerProjectionFixture(ready, {
  1675. id: "scenario-report-corrected-002",
  1676. type: "REPORT_CORRECTED",
  1677. occurredAt: "2026-07-03T11:00:00+08:00",
  1678. payload: {
  1679. reportId: "report-blood-20260702",
  1680. sourceVersion: 2,
  1681. sourceHash: "sha256:blood-v2"
  1682. }
  1683. }),
  1684. demoScenario: normalized
  1685. };
  1686. }
  1687. case "REPORT_READY": {
  1688. const report = applyServerProjectionFixture(base, reportFixture);
  1689. const pending = requestReportInterpretation(
  1690. report,
  1691. "report-blood-20260702"
  1692. );
  1693. const interpreting = grantConsent(
  1694. pending,
  1695. "consent-ai-report-blood-20260702"
  1696. );
  1697. return {
  1698. ...applyReportInterpretationProjectionFixture(
  1699. interpreting,
  1700. "report-blood-20260702"
  1701. ),
  1702. demoScenario: normalized
  1703. };
  1704. }
  1705. case "REGISTRATION_QUERY":
  1706. return { ...loadRegistrationScenario(base), demoScenario: normalized };
  1707. case "COMMAND_PREPARED":
  1708. return { ...loadCommandPreparedScenario(base), demoScenario: normalized };
  1709. case "APPOINTMENT_CREATED": {
  1710. const awaiting = loadCommandPreparedScenario(base);
  1711. return {
  1712. ...applyCommandProjectionFixture(awaiting, {
  1713. id: "scenario-command-succeeded-001",
  1714. commandId: "command-register-001",
  1715. status: "SUCCEEDED",
  1716. resultFixtureId: "appointment-created-001"
  1717. }),
  1718. activeTab: "home",
  1719. demoScenario: normalized
  1720. };
  1721. }
  1722. case "CHECKIN_AVAILABLE": {
  1723. const appointment = loadScenario(base, "APPOINTMENT_CREATED");
  1724. return {
  1725. ...applyServerProjectionFixture(appointment, {
  1726. id: "scenario-checkin-available-001",
  1727. type: "CHECKIN_AVAILABLE",
  1728. occurredAt: "2026-07-03T09:10:00+08:00",
  1729. payload: {
  1730. appointmentId: "appointment-001",
  1731. location: "\u95E8\u8BCA\u4E09\u697C\u795E\u7ECF\u5185\u79D1\u8BCA\u533A"
  1732. }
  1733. }),
  1734. demoScenario: normalized
  1735. };
  1736. }
  1737. case "QUEUE_UPDATED": {
  1738. const arrived = loadScenario(base, "CHECKIN_AVAILABLE");
  1739. const checkedIn = applyServerProjectionFixture(arrived, {
  1740. id: "scenario-checkin-completed-001",
  1741. type: "CHECKIN_COMPLETED",
  1742. occurredAt: "2026-07-03T09:12:00+08:00",
  1743. payload: {
  1744. appointmentId: "appointment-001",
  1745. queueNumber: "A023",
  1746. room: "3F-08\u8BCA\u5BA4"
  1747. }
  1748. });
  1749. return {
  1750. ...applyServerProjectionFixture(checkedIn, {
  1751. id: "scenario-queue-updated-001",
  1752. type: "QUEUE_UPDATED",
  1753. occurredAt: "2026-07-03T09:18:00+08:00",
  1754. payload: {
  1755. appointmentId: "appointment-001",
  1756. queueNumber: "A023",
  1757. peopleAhead: 4,
  1758. estimatedMinutes: 18,
  1759. room: "3F-08\u8BCA\u5BA4",
  1760. delayed: true
  1761. }
  1762. }),
  1763. demoScenario: normalized
  1764. };
  1765. }
  1766. default:
  1767. return base;
  1768. }
  1769. }
  1770. // docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js
  1771. var navItems = Object.freeze([
  1772. { id: "home", label: "\u9996\u9875", icon: "house" },
  1773. { id: "care", label: "\u5C31\u533B", icon: "stethoscope" },
  1774. { id: "assistant", label: "AI\u52A9\u624B", icon: "sparkles" },
  1775. { id: "records", label: "\u5065\u5EB7\u6863\u6848", icon: "folder-heart" },
  1776. { id: "profile", label: "\u6211\u7684", icon: "user-round" }
  1777. ]);
  1778. var demoScenarioGroups = Object.freeze({
  1779. REPORT_CLOSURE: Object.freeze([
  1780. "REPORT_ARRIVED",
  1781. "REPORT_DETAIL",
  1782. "REPORT_CONSENT",
  1783. "REPORT_GENERATING",
  1784. "REPORT_READY",
  1785. "REPORT_NEED_REVIEW",
  1786. "REPORT_CRITICAL",
  1787. "REPORT_INVALIDATED"
  1788. ]),
  1789. CARE_CLOSURE: Object.freeze([
  1790. "EMPTY_HOME",
  1791. "REGISTRATION_QUERY",
  1792. "COMMAND_PREPARED",
  1793. "APPOINTMENT_CREATED",
  1794. "CHECKIN_AVAILABLE",
  1795. "QUEUE_UPDATED"
  1796. ])
  1797. });
  1798. var demoScenarios = Object.freeze(
  1799. Object.values(demoScenarioGroups).flat()
  1800. );
  1801. var scenarioLabels = {
  1802. EMPTY_HOME: "\u7A7A\u95F2\u9996\u9875",
  1803. REPORT_ARRIVED: "\u62A5\u544A\u5230\u8FBE",
  1804. REPORT_DETAIL: "\u62A5\u544A\u8BE6\u60C5",
  1805. REPORT_CONSENT: "\u62A5\u544A\u6388\u6743",
  1806. REPORT_GENERATING: "\u89E3\u8BFB\u751F\u6210\u4E2D",
  1807. REPORT_READY: "\u62A5\u544A\u5DF2\u89E3\u8BFB",
  1808. REPORT_NEED_REVIEW: "\u62A5\u544A\u9700\u590D\u6838",
  1809. REPORT_CRITICAL: "\u62A5\u544A\u5371\u6025\u503C",
  1810. REPORT_INVALIDATED: "\u62A5\u544A\u5DF2\u5931\u6548",
  1811. REGISTRATION_QUERY: "\u81EA\u7136\u8BED\u8A00\u67E5\u53F7",
  1812. COMMAND_PREPARED: "\u6302\u53F7\u5F85\u786E\u8BA4",
  1813. APPOINTMENT_CREATED: "\u6302\u53F7\u6210\u529F",
  1814. CHECKIN_AVAILABLE: "\u5230\u9662\u53EF\u7B7E\u5230",
  1815. QUEUE_UPDATED: "\u5019\u8BCA\u66F4\u65B0"
  1816. };
  1817. var homeServices = Object.freeze([
  1818. { label: "\u9884\u7EA6\u6302\u53F7", icon: "calendar-plus" },
  1819. { label: "\u667A\u80FD\u5BFC\u8BCA", icon: "scan-heart" },
  1820. { label: "\u62A5\u544A\u67E5\u8BE2", icon: "file-chart-column" },
  1821. { label: "\u95E8\u8BCA\u7F34\u8D39", icon: "wallet-cards" },
  1822. { label: "\u68C0\u67E5\u9884\u7EA6", icon: "clipboard-clock" },
  1823. { label: "\u4F4F\u9662\u670D\u52A1", icon: "bed" },
  1824. { label: "\u9662\u5185\u5BFC\u822A", icon: "map-pinned" },
  1825. { label: "\u5728\u7EBF\u95EE\u8BCA", icon: "messages-square" }
  1826. ]);
  1827. var actionLabels = {
  1828. INTERPRET_REPORT: "AI \u89E3\u8BFB\u62A5\u544A",
  1829. VIEW_REPORT: "\u67E5\u770B\u539F\u62A5\u544A",
  1830. CONFIRM_CHECKIN: "\u786E\u8BA4\u7B7E\u5230",
  1831. VIEW_ROUTE: "\u67E5\u770B\u8DEF\u7EBF",
  1832. VIEW_QUEUE: "\u67E5\u770B\u5019\u8BCA"
  1833. };
  1834. var actionTokens = {
  1835. INTERPRET_REPORT: "interpret-report",
  1836. VIEW_REPORT: "view-report",
  1837. CONFIRM_CHECKIN: "open-checkin-confirmation",
  1838. VIEW_ROUTE: "open-route",
  1839. VIEW_QUEUE: "open-queue"
  1840. };
  1841. var statusPresentation = {
  1842. ACTION_REQUIRED: {
  1843. className: "action-required",
  1844. icon: "circle-alert",
  1845. label: "\u9700\u8981\u5904\u7406"
  1846. },
  1847. IN_PROGRESS: {
  1848. className: "in-progress",
  1849. icon: "loader-circle",
  1850. label: "\u8FDB\u884C\u4E2D"
  1851. },
  1852. UPCOMING: {
  1853. className: "upcoming",
  1854. icon: "calendar-clock",
  1855. label: "\u5373\u5C06\u5F00\u59CB"
  1856. },
  1857. COMPLETED: {
  1858. className: "completed",
  1859. icon: "circle-check-big",
  1860. label: "\u5DF2\u5B8C\u6210"
  1861. }
  1862. };
  1863. var unknownStatusPresentation = {
  1864. className: "unknown",
  1865. icon: "circle-help",
  1866. label: "\u72B6\u6001\u5F85\u786E\u8BA4"
  1867. };
  1868. var escapeHtml = (value) => String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
  1869. var getActionLabel = (action) => typeof action === "string" && Object.hasOwn(actionLabels, action) ? actionLabels[action] : null;
  1870. var renderActionAttributes = (action, task) => {
  1871. const token = typeof action === "string" && Object.hasOwn(actionTokens, action) ? actionTokens[action] : null;
  1872. if (!token) {
  1873. return "";
  1874. }
  1875. if (typeof task?.reportId === "string") {
  1876. return ` data-action="${token}" data-report-id="${escapeHtml(task.reportId)}"`;
  1877. }
  1878. if (typeof task?.appointmentId === "string") {
  1879. return ` data-action="${token}" data-appointment-id="${escapeHtml(task.appointmentId)}"`;
  1880. }
  1881. return "";
  1882. };
  1883. var getStatusPresentation = (status) => typeof status === "string" && Object.hasOwn(statusPresentation, status) ? statusPresentation[status] : unknownStatusPresentation;
  1884. var getShellElements = (root) => {
  1885. if (!root?.getElementById) {
  1886. return null;
  1887. }
  1888. const appHeader = root.getElementById("appHeader");
  1889. const appView = root.getElementById("appView");
  1890. const bottomNav = root.getElementById("bottomNav");
  1891. return appHeader && appView && bottomNav ? { appHeader, appView, bottomNav } : null;
  1892. };
  1893. var renderIcons = () => {
  1894. const lucide = globalThis.lucide;
  1895. if (lucide?.createIcons) {
  1896. lucide.createIcons({ attrs: { "aria-hidden": "true" } });
  1897. }
  1898. };
  1899. var patientDisplayNames = {
  1900. "patient-zhang-san": "\u5F20\u5148\u751F",
  1901. "patient-li-na": "\u674E\u5973\u58EB"
  1902. };
  1903. var patientDisplayName = (state) => patientDisplayNames[state?.activePatientId] ?? "\u5F53\u524D\u5C31\u8BCA\u4EBA";
  1904. var renderHeader = (state) => {
  1905. const messages = Array.isArray(state?.messages) ? state.messages : [];
  1906. const unreadCount = messages.filter((message) => message?.unread).length;
  1907. const messageLabel = unreadCount > 0 ? `\u6D88\u606F\uFF0C${unreadCount} \u6761\u672A\u8BFB` : "\u6D88\u606F";
  1908. return `
  1909. <div class="hospital-context">
  1910. <span class="hospital-logo" aria-hidden="true">
  1911. <i data-lucide="hospital"></i>
  1912. </span>
  1913. <div class="hospital-context__copy">
  1914. <p class="app-kicker">\u7A7A\u6D77\u533B\u9662</p>
  1915. <p class="campus-label">\u5F53\u524D\u9662\u533A \xB7 \u7A7A\u6D77\u533B\u9662\u672C\u9662</p>
  1916. </div>
  1917. <button class="icon-button" type="button" aria-label="${messageLabel}" data-action="open-messages">
  1918. <i data-lucide="bell"></i>
  1919. ${unreadCount > 0 ? `<span class="unread-badge" aria-hidden="true">${unreadCount}</span>` : ""}
  1920. </button>
  1921. <button class="icon-button" type="button" aria-label="\u9690\u79C1\u8BBE\u7F6E">
  1922. <i data-lucide="shield-check"></i>
  1923. </button>
  1924. </div>
  1925. <button class="patient-switcher" type="button" aria-label="\u5207\u6362\u5F53\u524D\u5C31\u8BCA\u4EBA" data-action="switch-patient">
  1926. <span>\u5F53\u524D\u5C31\u8BCA\u4EBA</span>
  1927. <strong>${patientDisplayName(state)}</strong>
  1928. <i data-lucide="chevron-down"></i>
  1929. </button>
  1930. `;
  1931. };
  1932. var journeyStages = [
  1933. { stage: "WAITING_ARRIVAL", label: "\u5230\u9662", time: "09:10" },
  1934. { stage: "CHECKIN_AVAILABLE", label: "\u7B7E\u5230", time: "09:20" },
  1935. { stage: "QUEUE_WAITING", label: "\u5019\u8BCA", time: "09:30" },
  1936. { stage: "VISIT_IN_PROGRESS", label: "\u533B\u751F\u63A5\u8BCA", time: "10:05" },
  1937. { stage: "POST_VISIT", label: "\u8BCA\u540E\u670D\u52A1", time: "10:25" }
  1938. ];
  1939. var renderJourneyProjection = (journey) => {
  1940. const activeIndex = Math.max(
  1941. 0,
  1942. journeyStages.findIndex(({ stage }) => stage === journey.stage)
  1943. );
  1944. const start = Math.max(
  1945. 0,
  1946. Math.min(activeIndex - 1, journeyStages.length - 3)
  1947. );
  1948. const visibleStages = journeyStages.slice(start, start + 3);
  1949. return `
  1950. <article class="journey-card">
  1951. <div class="journey-card__heading">
  1952. <div>
  1953. <strong>${escapeHtml(journey.title)}</strong>
  1954. <p>${escapeHtml(journey.location ?? journey.room ?? "\u9662\u5185\u884C\u7A0B\u6301\u7EED\u66F4\u65B0")}</p>
  1955. </div>
  1956. ${journey.stage === "QUEUE_WAITING" ? `
  1957. <button class="text-button" type="button" data-action="open-queue" data-appointment-id="${escapeHtml(journey.appointmentId)}">
  1958. \u67E5\u770B\u5019\u8BCA
  1959. </button>
  1960. ` : ""}
  1961. </div>
  1962. <ol class="journey-steps">
  1963. ${visibleStages.map((node) => `
  1964. <li class="${node.stage === journey.stage ? "is-current" : ""}">
  1965. <time>${node.time}</time>
  1966. <span>${node.label}</span>
  1967. </li>
  1968. `).join("")}
  1969. </ol>
  1970. </article>
  1971. `;
  1972. };
  1973. var renderHome = (state) => {
  1974. const tasks = selectHomeTasks(state);
  1975. const journeys = Array.isArray(state?.journeyProjections) ? state.journeyProjections : [];
  1976. const primaryTaskIndex = tasks.findIndex(
  1977. ({ primaryAction, status }) => (status === "ACTION_REQUIRED" || status === "IN_PROGRESS") && Boolean(getActionLabel(primaryAction))
  1978. );
  1979. const activeTaskCount = tasks.filter(
  1980. ({ status }) => status === "ACTION_REQUIRED" || status === "IN_PROGRESS"
  1981. ).length;
  1982. return `
  1983. <section class="ai-welcome" aria-labelledby="welcomeTitle">
  1984. <div>
  1985. <p class="section-eyebrow">AI \u5C31\u533B\u52A9\u624B</p>
  1986. <h1 id="welcomeTitle">${patientDisplayName(state)}\uFF0C\u4E0A\u5348\u597D</h1>
  1987. <p>${activeTaskCount > 0 ? `\u6709 ${activeTaskCount} \u9879\u5C31\u533B\u4E8B\u9879\u9700\u8981\u5173\u6CE8` : "\u4ECA\u5929\u6682\u65E0\u5F85\u5904\u7406\u7684\u5C31\u533B\u4E8B\u9879"}</p>
  1988. </div>
  1989. <img class="doctor-avatar" src="./assets/doctor.png" alt="\u7A7A\u6D77\u533B\u9662 AI \u533B\u751F\u52A9\u624B" />
  1990. <button class="assistant-input" type="button" data-action="open-assistant">
  1991. <i data-lucide="sparkles"></i>
  1992. <span>\u95EE\u75C5\u60C5\u3001\u67E5\u62A5\u544A\u3001\u627E\u533B\u751F\u3001\u529E\u5C31\u533B\u670D\u52A1</span>
  1993. <i data-lucide="arrow-up-right"></i>
  1994. </button>
  1995. </section>
  1996. <section class="home-section" aria-labelledby="tasksTitle">
  1997. <div class="section-heading">
  1998. <div>
  1999. <p class="section-eyebrow">\u5C31\u533B\u4EFB\u52A1</p>
  2000. <h2 id="tasksTitle">\u73B0\u5728\u8981\u505A</h2>
  2001. </div>
  2002. </div>
  2003. <div class="task-list">
  2004. ${tasks.length === 0 ? `
  2005. <div class="empty-card">
  2006. <i data-lucide="circle-check-big"></i>
  2007. <div>
  2008. <strong>\u4ECA\u5929\u6CA1\u6709\u5F85\u5904\u7406\u4E8B\u9879</strong>
  2009. <p>\u65B0\u7684\u62A5\u544A\u3001\u7F34\u8D39\u6216\u9884\u7EA6\u63D0\u9192\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002</p>
  2010. </div>
  2011. </div>
  2012. ` : tasks.map((task, index) => {
  2013. const isCompleted = task.status === "COMPLETED";
  2014. const status = getStatusPresentation(task.status);
  2015. const isPrimaryTask = index === primaryTaskIndex;
  2016. const primaryActionLabel = getActionLabel(task.primaryAction);
  2017. const secondaryActionLabel = getActionLabel(task.secondaryAction);
  2018. const canAct = (task.status === "ACTION_REQUIRED" || task.status === "IN_PROGRESS") && Boolean(primaryActionLabel);
  2019. const recommendation = isCompleted ? "\u65E0\u9700\u64CD\u4F5C" : canAct ? primaryActionLabel : task.status === "UPCOMING" ? "\u7B49\u5F85\u4E8B\u9879\u5F00\u59CB" : "\u786E\u8BA4\u72B6\u6001\u4E0E\u53EF\u7528\u64CD\u4F5C";
  2020. return `
  2021. <article class="task-card task-card--${status.className}">
  2022. <div class="task-card__status">
  2023. <i data-lucide="${status.icon}"></i>
  2024. <span>${status.label}</span>
  2025. </div>
  2026. <h3>${escapeHtml(task.title)}</h3>
  2027. <p><strong>\u53D1\u751F\u4E86\u4EC0\u4E48\uFF1A</strong>${escapeHtml(task.description)}</p>
  2028. <p><strong>\u4E3A\u4EC0\u4E48\u5173\u6CE8\uFF1A</strong>${isCompleted ? "\u8BE5\u4E8B\u9879\u5DF2\u5904\u7406\u5B8C\u6210\u3002" : "\u53CA\u65F6\u5904\u7406\u6709\u52A9\u4E8E\u987A\u5229\u63A8\u8FDB\u540E\u7EED\u5C31\u533B\u5B89\u6392\u3002"}</p>
  2029. <div class="task-card__recommendation">
  2030. <span>\u5EFA\u8BAE\u64CD\u4F5C</span>
  2031. <strong>${recommendation}</strong>
  2032. </div>
  2033. ${canAct ? `
  2034. <div class="task-card__actions">
  2035. <button class="button ${isPrimaryTask ? "button--primary" : "button--secondary"}" type="button"${renderActionAttributes(task.primaryAction, task)}>
  2036. ${primaryActionLabel}
  2037. </button>
  2038. ${secondaryActionLabel ? `
  2039. <button class="button button--secondary" type="button"${renderActionAttributes(task.secondaryAction, task)}>
  2040. ${secondaryActionLabel}
  2041. </button>
  2042. ` : ""}
  2043. </div>
  2044. ` : ""}
  2045. </article>
  2046. `;
  2047. }).join("")}
  2048. </div>
  2049. </section>
  2050. <section class="home-section" aria-labelledby="journeyTitle">
  2051. <div class="section-heading">
  2052. <div>
  2053. <p class="section-eyebrow">\u9662\u5185\u8FDB\u5EA6</p>
  2054. <h2 id="journeyTitle">\u4ECA\u65E5\u65C5\u7A0B</h2>
  2055. </div>
  2056. </div>
  2057. ${journeys.length > 0 ? journeys.map(renderJourneyProjection).join("") : `
  2058. <div class="journey-empty">
  2059. <i data-lucide="route"></i>
  2060. <div>
  2061. <strong>\u4ECA\u5929\u6682\u65E0\u9662\u5185\u884C\u7A0B</strong>
  2062. <p>\u6302\u53F7\u6216\u68C0\u67E5\u5B89\u6392\u786E\u8BA4\u540E\uFF0C\u5C06\u5C55\u793A\u524D\u540E\u6B65\u9AA4\u548C\u5F53\u524D\u8282\u70B9\u3002</p>
  2063. </div>
  2064. </div>
  2065. `}
  2066. </section>
  2067. <section class="home-section" aria-labelledby="servicesTitle">
  2068. <div class="section-heading">
  2069. <div>
  2070. <p class="section-eyebrow">\u5FEB\u6377\u5165\u53E3</p>
  2071. <h2 id="servicesTitle">\u5E38\u7528\u670D\u52A1</h2>
  2072. </div>
  2073. </div>
  2074. <div class="service-grid">
  2075. ${homeServices.map(({ label, icon }) => `
  2076. <button class="service-entry" type="button">
  2077. <span><i data-lucide="${icon}"></i></span>
  2078. <strong>${label}</strong>
  2079. </button>
  2080. `).join("")}
  2081. </div>
  2082. </section>
  2083. <section class="home-section care-section" aria-labelledby="careTitle">
  2084. <div class="section-heading">
  2085. <div>
  2086. <p class="section-eyebrow">\u5065\u5EB7\u63D0\u9192</p>
  2087. <h2 id="careTitle">\u4E3B\u52A8\u5173\u6000</h2>
  2088. </div>
  2089. </div>
  2090. <div class="care-empty">
  2091. <i data-lucide="heart-handshake"></i>
  2092. <div>
  2093. <strong>\u6682\u65E0\u65B0\u7684\u4E3B\u52A8\u5173\u6000\u63D0\u9192</strong>
  2094. <p>\u7528\u836F\u3001\u590D\u8BCA\u4E0E\u68C0\u67E5\u51C6\u5907\u63D0\u9192\u4F1A\u5728\u786E\u8BA4\u540E\u5C55\u793A\u3002</p>
  2095. </div>
  2096. </div>
  2097. </section>
  2098. `;
  2099. };
  2100. var candidateToken = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
  2101. var renderAssistantMessages = (messages) => Array.isArray(messages) ? messages.filter((message) => message && typeof message === "object" && (message.role === "user" || message.role === "assistant")).map(({ role, text }) => `
  2102. <article class="assistant-message assistant-message--${role}">
  2103. <strong>${role === "user" ? "\u4F60" : "AI \u52A9\u624B"}</strong>
  2104. <p>${escapeHtml(text)}</p>
  2105. </article>
  2106. `).join("") : "";
  2107. var renderAssistant = (state) => {
  2108. const task = state?.activeAgentTaskProjection;
  2109. const result = typeof task?.activeResultRef === "string" ? state?.resultSnapshotProjections?.[task.activeResultRef] : null;
  2110. const items = task?.stage === "CANDIDATE_SELECTION" && result?.resultRef === task.activeResultRef && Array.isArray(result?.items) ? result.items : [];
  2111. const candidates = items.filter((item) => item && typeof item === "object" && typeof item.candidateId === "string" && candidateToken.test(item.candidateId));
  2112. const hasTask = task?.type === "REGISTRATION";
  2113. return `
  2114. <section class="assistant-view" aria-labelledby="assistantTitle">
  2115. <header class="assistant-view__header">
  2116. <button class="text-button" type="button" data-action="go-home">
  2117. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  2118. </button>
  2119. <div>
  2120. <p class="section-eyebrow">\u81EA\u7136\u8BED\u8A00\u5C31\u533B\u670D\u52A1</p>
  2121. <h1 id="assistantTitle">AI \u52A9\u624B</h1>
  2122. </div>
  2123. ${hasTask ? `
  2124. <button class="text-button" type="button" data-action="return-current-task">\u8FD4\u56DE\u5F53\u524D\u4EFB\u52A1</button>
  2125. ` : ""}
  2126. </header>
  2127. <section class="assistant-scenarios" aria-labelledby="scenarioTitle">
  2128. <h2 id="scenarioTitle">\u573A\u666F\u5EFA\u8BAE</h2>
  2129. <button type="button" data-action="open-report-center" data-filter="ALL">\u5E2E\u6211\u770B\u62A5\u544A</button>
  2130. <button type="button" data-action="submit-registration-demo">\u6211\u60F3\u6302\u660E\u5929\u795E\u7ECF\u5185\u79D1\u674E\u660E\u4E3B\u4EFB\u7684\u53F7</button>
  2131. <button type="button">\u667A\u80FD\u5BFC\u8BCA</button>
  2132. <button type="button">\u4EBA\u5DE5\u670D\u52A1</button>
  2133. </section>
  2134. <div class="assistant-thread" aria-live="polite">
  2135. ${renderAssistantMessages(state?.messages)}
  2136. ${task?.stage === "REQUERY_REQUIRED" ? `
  2137. <aside class="assistant-recovery">
  2138. <strong>REQUERY_REQUIRED</strong>
  2139. <p>\u65E7\u5019\u9009\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u540E\u518D\u9009\u62E9\u3002</p>
  2140. <button class="button button--secondary" type="button" data-action="submit-registration-demo">\u91CD\u65B0\u67E5\u8BE2</button>
  2141. </aside>
  2142. ` : ""}
  2143. ${result ? `
  2144. <section class="assistant-result" aria-labelledby="slotResultTitle">
  2145. <h2 id="slotResultTitle">\u53EF\u9009\u53F7\u6E90</h2>
  2146. <p class="assistant-answer">${escapeHtml(result.text)}</p>
  2147. ${candidates.length > 0 ? `
  2148. <div class="slot-presentation">
  2149. ${candidates.map((item) => `
  2150. <button class="slot-candidate" type="button" data-action="select-candidate" data-candidate-id="${item.candidateId}">
  2151. <strong>${escapeHtml(item.visitDate)} ${escapeHtml(item.timePeriod)} ${escapeHtml(item.visitTime)}</strong>
  2152. <span>${escapeHtml(item.departmentName)} \xB7 ${escapeHtml(item.doctorName)} ${escapeHtml(item.doctorTitle)}</span>
  2153. <span>\u4F59\u53F7 ${escapeHtml(item.remainingSlots)} \xB7 \u6302\u53F7\u8D39 ${escapeHtml(item.fee)}</span>
  2154. </button>
  2155. `).join("")}
  2156. </div>
  2157. ` : ""}
  2158. </section>
  2159. ` : ""}
  2160. ${task?.stage === "AWAITING_REGISTRATION_CONFIRMATION" ? `
  2161. <aside class="assistant-lock-success">
  2162. <strong>\u9501\u53F7\u6210\u529F</strong>
  2163. <p>\u53F7\u6E90\u5DF2\u4E34\u65F6\u9501\u5B9A\uFF0C\u5DF2\u6536\u5230\u670D\u52A1\u7AEF\u4E8C\u6B21\u786E\u8BA4\u6295\u5F71\u3002</p>
  2164. </aside>
  2165. ` : ""}
  2166. </div>
  2167. <div class="assistant-composer">
  2168. <label class="sr-only" for="assistantComposerInput">\u8F93\u5165\u5C31\u533B\u9700\u6C42</label>
  2169. <input id="assistantComposerInput" type="text" placeholder="\u63CF\u8FF0\u4F60\u60F3\u529E\u7406\u7684\u5C31\u533B\u670D\u52A1" />
  2170. <button type="button" data-action="submit-assistant-text" aria-label="\u53D1\u9001">
  2171. <i data-lucide="arrow-up"></i>
  2172. </button>
  2173. </div>
  2174. </section>
  2175. `;
  2176. };
  2177. var reportFamilyLabels = { LAB: "\u68C0\u9A8C\u62A5\u544A", EXAM: "\u68C0\u67E5\u62A5\u544A" };
  2178. var reportSourceStatusLabels = {
  2179. FINAL: "\u5DF2\u5BA1\u6838",
  2180. PRELIMINARY: "\u5F85\u5BA1\u6838"
  2181. };
  2182. var reportInterpretationLabels = {
  2183. NOT_REQUESTED: "\u5C1A\u672A\u8FDB\u884C AI \u8F85\u52A9\u89E3\u8BFB",
  2184. INTERPRETING: "AI \u8F85\u52A9\u89E3\u8BFB\u4E2D",
  2185. READY: "AI \u8F85\u52A9\u89E3\u8BFB\u5DF2\u751F\u6210",
  2186. NEED_REVIEW: "\u89E3\u8BFB\u7ED3\u679C\u5EFA\u8BAE\u7531\u533B\u751F\u786E\u8BA4"
  2187. };
  2188. var metricFlagLabels = { HIGH: "\u504F\u9AD8", LOW: "\u504F\u4F4E", NORMAL: "\u6B63\u5E38" };
  2189. var abnormalMetricsOf = (report) => Array.isArray(report?.metrics) ? report.metrics.filter((metric) => ["HIGH", "LOW"].includes(metric?.sourceFlag)) : [];
  2190. var renderMetric = (metric) => {
  2191. const name = escapeHtml(metric?.name || "\u672A\u547D\u540D\u6307\u6807");
  2192. const value = metric?.value === null || metric?.value === void 0 ? "\u2014" : escapeHtml(metric.value);
  2193. const unit = metric?.unit ? ` ${escapeHtml(metric.unit)}` : "";
  2194. const range = metric?.referenceRange ? `<small>\u53C2\u8003\u8303\u56F4 ${escapeHtml(metric.referenceRange)}${unit}</small>` : "<small>\u53C2\u8003\u8303\u56F4\u672A\u63D0\u4F9B</small>";
  2195. const flag = metricFlagLabels[metric?.sourceFlag] ?? "\u7ED3\u679C\u5F85\u786E\u8BA4";
  2196. return `<li class="report-metric report-metric--${escapeHtml(String(metric?.sourceFlag ?? "unknown").toLowerCase())}">
  2197. <span>${name}</span><strong>${value}${unit}</strong>${range}<em>${flag}</em>
  2198. </li>`;
  2199. };
  2200. var renderReportCenter = (state) => {
  2201. const filters = {
  2202. family: ["ALL", "LAB", "EXAM"].includes(state?.reportFilters?.family) ? state.reportFilters.family : "ALL",
  2203. status: ["ALL", "UNREAD", "READ"].includes(state?.reportFilters?.status) ? state.reportFilters.status : "ALL"
  2204. };
  2205. const reports = selectReports(state, filters);
  2206. return `
  2207. <section class="module-page report-center" aria-labelledby="reportCenterTitle">
  2208. ${pageHeader("\u5065\u5EB7\u6863\u6848", "\u62A5\u544A\u4E2D\u5FC3", "\u533B\u9662\u539F\u59CB\u62A5\u544A\u548C AI \u8F85\u52A9\u89E3\u8BFB\u7EDF\u4E00\u67E5\u770B\u3002")}
  2209. <p class="report-center__patient">\u5F53\u524D\u5C31\u8BCA\u4EBA ${escapeHtml(patientDisplayName(state))}</p>
  2210. <div class="report-filter-group" role="group" aria-label="\u62A5\u544A\u7C7B\u578B">
  2211. ${[["ALL", "\u5168\u90E8"], ["LAB", "\u68C0\u9A8C\u62A5\u544A"], ["EXAM", "\u68C0\u67E5\u62A5\u544A"]].map(([id, label]) => `
  2212. <button type="button" data-action="set-report-family-filter" data-filter="${id}" aria-pressed="${filters.family === id}">${label}</button>
  2213. `).join("")}
  2214. </div>
  2215. <div class="report-filter-group report-filter-group--status" role="group" aria-label="\u9605\u8BFB\u72B6\u6001">
  2216. ${[["ALL", "\u5168\u90E8"], ["UNREAD", "\u672A\u8BFB"], ["READ", "\u5DF2\u8BFB"]].map(([id, label]) => `
  2217. <button type="button" data-action="set-report-status-filter" data-filter="${id}" aria-pressed="${filters.status === id}">${label}</button>
  2218. `).join("")}
  2219. </div>
  2220. <div class="report-center__list">
  2221. ${reports.length ? reports.map((report) => {
  2222. const abnormalCount = abnormalMetricsOf(report).length;
  2223. return `
  2224. <button class="report-list-card" type="button" data-action="view-report" data-report-id="${escapeHtml(report.reportId)}">
  2225. <span class="report-list-card__top"><b>${escapeHtml(reportFamilyLabels[report.family] ?? "\u533B\u9662\u62A5\u544A")}</b><em>${report.readStatus === "READ" ? "\u5DF2\u8BFB" : "\u672A\u8BFB"}</em></span>
  2226. <strong>${escapeHtml(report.title || "\u672A\u547D\u540D\u62A5\u544A")}</strong>
  2227. <span>${escapeHtml(report.hospitalName || "\u533B\u9662\u4FE1\u606F\u5F85\u786E\u8BA4")} \xB7 ${escapeHtml(report.departmentName || "\u79D1\u5BA4\u5F85\u786E\u8BA4")}</span>
  2228. <time>${escapeHtml(report.issuedAt || "\u51FA\u5177\u65F6\u95F4\u5F85\u786E\u8BA4")}</time>
  2229. <span class="report-list-card__status">${escapeHtml(reportSourceStatusLabels[report.sourceStatus] ?? "\u62A5\u544A\u72B6\u6001\u5F85\u786E\u8BA4")}${report.family === "LAB" ? ` \xB7 ${abnormalCount ? `${abnormalCount} \u9879\u6307\u6807\u5F02\u5E38` : "\u672A\u6807\u8BB0\u5F02\u5E38\u6307\u6807"}` : ""}</span>
  2230. <span>${report.family === "EXAM" ? "\u8BE5\u7C7B\u578B\u6682\u672A\u5F00\u653EAI\u8F85\u52A9\u89E3\u8BFB" : escapeHtml(reportInterpretationLabels[report.interpretationStatus] ?? "AI \u8F85\u52A9\u89E3\u8BFB\u72B6\u6001\u5F85\u786E\u8BA4")}</span>
  2231. </button>`;
  2232. }).join("") : `<div class="report-empty"><i data-lucide="file-search"></i><strong>\u6682\u65E0\u7B26\u5408\u7B5B\u9009\u6761\u4EF6\u7684\u62A5\u544A</strong><p>\u53EF\u4EE5\u8C03\u6574\u62A5\u544A\u7C7B\u578B\u6216\u9605\u8BFB\u72B6\u6001\u3002</p></div>`}
  2233. </div>
  2234. </section>`;
  2235. };
  2236. var renderReportDetail = (report = {}, expanded = false) => {
  2237. const metrics = Array.isArray(report?.metrics) ? report.metrics : [];
  2238. const abnormalMetrics = abnormalMetricsOf(report);
  2239. const hiddenMetrics = metrics.filter(
  2240. (metric) => !abnormalMetrics.includes(metric)
  2241. );
  2242. const visibleMetrics = expanded ? [...abnormalMetrics, ...hiddenMetrics] : abnormalMetrics;
  2243. const reportId = report?.reportId ?? "";
  2244. return `
  2245. <section class="report-view" aria-labelledby="reportTitle">
  2246. <button class="text-button report-view__back" type="button" data-action="open-report-center">
  2247. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u62A5\u544A\u4E2D\u5FC3
  2248. </button>
  2249. <div class="report-hero">
  2250. <p class="section-eyebrow">\u533B\u9662\u539F\u59CB\u62A5\u544A</p>
  2251. <h1 id="reportTitle">${escapeHtml(report?.title || "\u672A\u547D\u540D\u62A5\u544A")}</h1>
  2252. <p>${escapeHtml(report?.hospitalName || "\u533B\u9662\u4FE1\u606F\u5F85\u786E\u8BA4")} \xB7 ${escapeHtml(report?.departmentName || "\u79D1\u5BA4\u5F85\u786E\u8BA4")}</p>
  2253. <p>\u51FA\u5177\u65F6\u95F4\uFF1A${escapeHtml(report?.issuedAt || "\u5F85\u786E\u8BA4")}</p>
  2254. <span class="report-audit"><i data-lucide="badge-check"></i>${escapeHtml(reportSourceStatusLabels[report?.sourceStatus] ?? "\u62A5\u544A\u72B6\u6001\u5F85\u786E\u8BA4")}</span>
  2255. </div>
  2256. <div class="report-notice">
  2257. <i data-lucide="shield-alert"></i>
  2258. <p><strong>AI \u8F85\u52A9\u89E3\u8BFB\u4EC5\u4F9B\u53C2\u8003</strong>\uFF0C\u62A5\u544A\u7ED3\u679C\u9700\u7ED3\u5408\u75C7\u72B6\u3001\u75C5\u53F2\u5E76\u7531\u533B\u751F\u5224\u65AD\u3002</p>
  2259. </div>
  2260. <section class="report-card" aria-labelledby="abnormalTitle">
  2261. <div class="section-heading">
  2262. <div>
  2263. <p class="section-eyebrow">\u6307\u6807\u6982\u89C8</p>
  2264. <h2 id="abnormalTitle">${abnormalMetrics.length ? `${abnormalMetrics.length} \u9879\u5F02\u5E38\u6307\u6807` : "\u672A\u6807\u8BB0\u5F02\u5E38\u6307\u6807"}</h2>
  2265. </div>
  2266. </div>
  2267. <ul class="indicator-list">
  2268. ${visibleMetrics.length ? visibleMetrics.map(renderMetric).join("") : '<li class="report-metric"><span>\u6682\u65E0\u6307\u6807\u6570\u636E</span></li>'}
  2269. </ul>
  2270. ${hiddenMetrics.length ? `
  2271. <button class="button button--secondary" type="button" data-action="toggle-report-metrics">
  2272. ${expanded ? "\u6536\u8D77\u5168\u90E8\u6307\u6807" : "\u67E5\u770B\u5168\u90E8\u6307\u6807"}
  2273. </button>
  2274. ` : ""}
  2275. </section>
  2276. <div class="report-links" aria-label="\u62A5\u544A\u76F8\u5173\u5165\u53E3">
  2277. <button type="button" data-action="view-original-report" data-report-id="${escapeHtml(reportId)}"><i data-lucide="file-text"></i>\u67E5\u770B\u539F\u59CB\u62A5\u544A</button>
  2278. </div>
  2279. <button class="button button--primary report-primary" type="button" data-action="interpret-report" data-report-id="${escapeHtml(reportId)}">
  2280. <i data-lucide="sparkles"></i>AI \u8F85\u52A9\u89E3\u8BFB
  2281. </button>
  2282. </section>
  2283. `;
  2284. };
  2285. var safeReportText = (value, fallback) => typeof value === "string" && value.trim() ? escapeHtml(value.trim()) : fallback;
  2286. var renderExaminationReportDetail = (report = {}) => {
  2287. const reportId = typeof report?.reportId === "string" ? report.reportId : "";
  2288. return `
  2289. <section class="report-view examination-report" aria-labelledby="examinationReportTitle">
  2290. <button class="text-button report-view__back" type="button" data-action="open-report-center">
  2291. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u62A5\u544A\u4E2D\u5FC3
  2292. </button>
  2293. <div class="report-hero">
  2294. <p class="section-eyebrow">\u533B\u9662\u68C0\u67E5\u62A5\u544A</p>
  2295. <h1 id="examinationReportTitle">${safeReportText(report?.title, "\u68C0\u67E5\u540D\u79F0\u5F85\u786E\u8BA4")}</h1>
  2296. <p>\u68C0\u67E5\u90E8\u4F4D\uFF1A${safeReportText(report?.bodyPart, "\u5F85\u786E\u8BA4")}</p>
  2297. <p>${safeReportText(report?.hospitalName, "\u533B\u9662\u4FE1\u606F\u5F85\u786E\u8BA4")} \xB7 ${safeReportText(report?.departmentName, "\u79D1\u5BA4\u5F85\u786E\u8BA4")}</p>
  2298. <p>\u51FA\u5177\u65F6\u95F4\uFF1A${safeReportText(report?.issuedAt, "\u5F85\u786E\u8BA4")}</p>
  2299. </div>
  2300. <section class="report-card" aria-labelledby="examinationFindingsTitle">
  2301. <p class="section-eyebrow">\u68C0\u67E5\u6240\u89C1</p>
  2302. <h2 id="examinationFindingsTitle">\u68C0\u67E5\u6240\u89C1</h2>
  2303. <p>${safeReportText(report?.findings, "\u68C0\u67E5\u6240\u89C1\u5F85\u786E\u8BA4")}</p>
  2304. </section>
  2305. <section class="report-card" aria-labelledby="examinationConclusionTitle">
  2306. <p class="section-eyebrow">\u62A5\u544A\u7ED3\u8BBA</p>
  2307. <h2 id="examinationConclusionTitle">\u62A5\u544A\u7ED3\u8BBA</h2>
  2308. <p>${safeReportText(report?.conclusion, "\u62A5\u544A\u7ED3\u8BBA\u5F85\u786E\u8BA4")}</p>
  2309. </section>
  2310. <section class="report-card examination-report__audit" aria-label="\u62A5\u544A\u5BA1\u6838\u4FE1\u606F">
  2311. <p><strong>\u62A5\u544A\u533B\u751F\uFF1A</strong>${safeReportText(report?.reportingDoctor, "\u62A5\u544A\u533B\u751F\u5F85\u786E\u8BA4")}</p>
  2312. <p><strong>\u5BA1\u6838\u72B6\u6001\uFF1A</strong>${safeReportText(
  2313. report?.reviewStatus,
  2314. reportSourceStatusLabels[report?.sourceStatus] ?? "\u5BA1\u6838\u72B6\u6001\u5F85\u786E\u8BA4"
  2315. )}</p>
  2316. </section>
  2317. <div class="report-links" aria-label="\u68C0\u67E5\u62A5\u544A\u539F\u4EF6\u5165\u53E3">
  2318. <button type="button" data-action="view-original-report" data-report-id="${escapeHtml(reportId)}">
  2319. <i data-lucide="images"></i>\u67E5\u770B\u56FE\u50CF\u6216PDF
  2320. </button>
  2321. </div>
  2322. <div class="report-notice">
  2323. <i data-lucide="info"></i>
  2324. <p>\u8BE5\u7C7B\u578B\u6682\u672A\u5F00\u653EAI\u8F85\u52A9\u89E3\u8BFB</p>
  2325. </div>
  2326. </section>
  2327. `;
  2328. };
  2329. var renderOriginalReport = (report = {}) => {
  2330. if (report?.family === "EXAM") {
  2331. return `
  2332. <section class="report-view original-report examination-original" aria-labelledby="originalExaminationTitle">
  2333. <button class="text-button report-view__back" type="button" data-action="view-report" data-report-id="${escapeHtml(report?.reportId)}">
  2334. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u62A5\u544A\u8BE6\u60C5
  2335. </button>
  2336. <div class="report-hero">
  2337. <p class="section-eyebrow">\u533B\u9662\u68C0\u67E5\u62A5\u544A\u539F\u4EF6</p>
  2338. <h1 id="originalExaminationTitle">${safeReportText(report?.title, "\u68C0\u67E5\u540D\u79F0\u5F85\u786E\u8BA4")}</h1>
  2339. <p>${safeReportText(report?.originalFormat, "\u56FE\u50CF\u6216PDF")}</p>
  2340. </div>
  2341. <div class="report-notice">
  2342. <i data-lucide="file-check"></i>
  2343. <p>\u539F\u59CB\u56FE\u50CF\u6216PDF\u7531\u533B\u9662\u62A5\u544A\u7CFB\u7EDF\u63D0\u4F9B\uFF0C\u672C\u539F\u578B\u5C55\u793A\u7EDF\u4E00\u67E5\u770B\u5165\u53E3\u3002</p>
  2344. </div>
  2345. </section>
  2346. `;
  2347. }
  2348. const metrics = Array.isArray(report?.metrics) ? report.metrics : [];
  2349. return `
  2350. <section class="report-view original-report" aria-labelledby="originalReportTitle">
  2351. <button class="text-button report-view__back" type="button" data-action="view-report" data-report-id="${escapeHtml(report?.reportId)}">
  2352. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u62A5\u544A\u8BE6\u60C5
  2353. </button>
  2354. <div class="report-hero">
  2355. <p class="section-eyebrow">\u533B\u9662\u539F\u59CB\u62A5\u544A</p>
  2356. <h1 id="originalReportTitle">\u539F\u59CB\u62A5\u544A\u5185\u5BB9</h1>
  2357. <p>${escapeHtml(report?.title || "\u672A\u547D\u540D\u62A5\u544A")}</p>
  2358. <p>${escapeHtml(report?.hospitalName || "\u533B\u9662\u4FE1\u606F\u5F85\u786E\u8BA4")} \xB7 ${escapeHtml(report?.departmentName || "\u79D1\u5BA4\u5F85\u786E\u8BA4")}</p>
  2359. <p>\u51FA\u5177\u65F6\u95F4\uFF1A${escapeHtml(report?.issuedAt || "\u5F85\u786E\u8BA4")}</p>
  2360. </div>
  2361. <section class="report-card" aria-label="\u539F\u59CB\u6307\u6807">
  2362. <ul class="indicator-list">
  2363. ${metrics.length ? metrics.map(renderMetric).join("") : '<li class="report-metric"><span>\u6682\u65E0\u6307\u6807\u6570\u636E</span></li>'}
  2364. </ul>
  2365. </section>
  2366. </section>
  2367. `;
  2368. };
  2369. var renderConsentOverlay = (state = {}) => {
  2370. const reportId = state?.overlays?.reportId;
  2371. const report = state?.reportSourceProjections?.[reportId];
  2372. return `
  2373. <div class="consent-backdrop" role="presentation">
  2374. <section class="consent-sheet" role="dialog" aria-modal="true" aria-labelledby="consentTitle" tabindex="-1">
  2375. <div class="consent-sheet__icon"><i data-lucide="shield-check"></i></div>
  2376. <p class="section-eyebrow">\u9690\u79C1\u6388\u6743</p>
  2377. <h2 id="consentTitle">\u6388\u6743 AI \u8F85\u52A9\u89E3\u91CA\u672C\u6B21\u62A5\u544A</h2>
  2378. <dl class="consent-details">
  2379. <div><dt>\u5C31\u8BCA\u4EBA</dt><dd>${escapeHtml(patientDisplayName(state))}</dd></div>
  2380. <div><dt>\u7528\u9014</dt><dd>\u4EC5\u7528\u4E8E\u8FD9\u4EFD\u62A5\u544A\u7684 AI \u8F85\u52A9\u89E3\u91CA</dd></div>
  2381. <div><dt>\u8303\u56F4</dt><dd>${escapeHtml(report?.title || "\u5F53\u524D\u8840\u5E38\u89C4\u62A5\u544A")}</dd></div>
  2382. <div><dt>\u6709\u6548\u671F</dt><dd>\u81F3 2026-07-09 23:59</dd></div>
  2383. </dl>
  2384. <div class="consent-note">
  2385. <p><strong>AI \u8F85\u52A9\u6027\u8D28\uFF1A</strong>\u7528\u4E8E\u5E2E\u52A9\u7406\u89E3\u6307\u6807\uFF0C\u4E0D\u66FF\u4EE3\u533B\u751F\u8BCA\u65AD\u6216\u8BCA\u7597\u5EFA\u8BAE\u3002</p>
  2386. <p><strong>\u98CE\u9669\u8BF4\u660E\uFF1A</strong>\u89E3\u91CA\u53EF\u80FD\u5B58\u5728\u9057\u6F0F\u6216\u504F\u5DEE\uFF0C\u8BF7\u7ED3\u5408\u75C7\u72B6\u7531\u533B\u751F\u5224\u65AD\u3002</p>
  2387. <p><strong>\u64A4\u56DE\u65B9\u5F0F\uFF1A</strong>\u53EF\u968F\u65F6\u5728\u9690\u79C1\u8BBE\u7F6E\u4E2D\u64A4\u56DE\uFF0C\u64A4\u56DE\u540E\u4E0D\u518D\u5C55\u793A\u5386\u53F2\u89E3\u91CA\u3002</p>
  2388. </div>
  2389. <div class="consent-actions">
  2390. <button class="button button--primary" type="button" data-action="grant-report-consent">\u540C\u610F\u5E76\u7EE7\u7EED</button>
  2391. <button class="button button--secondary" type="button" data-action="reject-report-consent">\u6682\u4E0D\u6388\u6743</button>
  2392. </div>
  2393. </section>
  2394. </div>
  2395. `;
  2396. };
  2397. var commandPresentation = {
  2398. PREPARED: { label: "\u51C6\u5907\u4E2D", icon: "loader-circle", tone: "pending" },
  2399. AWAITING_CONFIRMATION: { label: "\u5F85\u786E\u8BA4", icon: "shield-check", tone: "warning" },
  2400. EXECUTING: { label: "\u6267\u884C\u4E2D", icon: "loader-circle", tone: "pending" },
  2401. SUCCEEDED: { label: "\u6302\u53F7\u6210\u529F", icon: "circle-check-big", tone: "success" },
  2402. FAILED: { label: "\u6267\u884C\u5931\u8D25", icon: "circle-x", tone: "danger" },
  2403. EXPIRED: { label: "\u5DF2\u8FC7\u671F", icon: "clock-alert", tone: "danger" },
  2404. UNKNOWN: { label: "\u7ED3\u679C\u786E\u8BA4\u4E2D\uFF0C\u8BF7\u52FF\u91CD\u590D\u64CD\u4F5C", icon: "circle-help", tone: "warning" },
  2405. REJECTED: { label: "\u5DF2\u6682\u4E0D\u64CD\u4F5C", icon: "circle-minus", tone: "muted" }
  2406. };
  2407. var renderCommandOverlay = (command) => {
  2408. const presentation = commandPresentation[command?.status] ?? commandPresentation.PREPARED;
  2409. const details = command?.details ?? {};
  2410. const statusLabel = command?.status === "SUCCEEDED" && !details.patientName ? "\u6A21\u62DF\u5B8C\u6210" : presentation.label;
  2411. const awaiting = command?.status === "AWAITING_CONFIRMATION";
  2412. const canRequery = command?.status === "EXPIRED" || command?.status === "FAILED";
  2413. const canClose = command?.status === "SUCCEEDED" || command?.status === "REJECTED";
  2414. return `
  2415. <div class="consent-backdrop command-backdrop" role="presentation">
  2416. <section class="consent-sheet command-sheet command-sheet--${presentation.tone}" role="dialog" aria-modal="true" aria-labelledby="commandTitle" tabindex="-1">
  2417. <div class="command-status">
  2418. <i data-lucide="${presentation.icon}"></i>
  2419. <strong>${statusLabel}</strong>
  2420. </div>
  2421. <p class="section-eyebrow">\u53D7\u63A7\u4E1A\u52A1\u64CD\u4F5C</p>
  2422. <h2 id="commandTitle">${escapeHtml(details.operationName ?? "\u786E\u8BA4\u6302\u53F7")}</h2>
  2423. <dl class="command-details">
  2424. <div><dt>\u5C31\u8BCA\u4EBA</dt><dd>${escapeHtml(details.patientName)}</dd></div>
  2425. <div><dt>\u79D1\u5BA4 / \u533B\u751F</dt><dd>${escapeHtml(details.departmentName)} \xB7 ${escapeHtml(details.doctorName)}${escapeHtml(details.doctorTitle)}</dd></div>
  2426. <div><dt>\u5C31\u8BCA\u65F6\u95F4</dt><dd>${escapeHtml(details.visitAt)}</dd></div>
  2427. <div><dt>\u91D1\u989D</dt><dd>${escapeHtml(details.fee)}</dd></div>
  2428. <div><dt>\u6709\u6548\u65F6\u95F4</dt><dd>${escapeHtml(details.countdown ?? details.expiresAt)}</dd></div>
  2429. </dl>
  2430. ${command?.status === "UNKNOWN" ? `
  2431. <p class="command-guidance">\u7ED3\u679C\u786E\u8BA4\u4E2D\uFF0C\u8BF7\u52FF\u91CD\u590D\u64CD\u4F5C\u3002\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u67E5\u8BE2\u6700\u7EC8\u7ED3\u679C\u3002</p>
  2432. ` : ""}
  2433. ${command?.status === "FAILED" ? `
  2434. <p class="command-guidance">\u672C\u6B21\u64CD\u4F5C\u672A\u6210\u529F\u3002\u8BF7\u91CD\u65B0\u67E5\u8BE2\u53F7\u6E90\uFF0C\u4E0D\u76F4\u63A5\u91CD\u8BD5\u8BE5 Command\u3002</p>
  2435. ` : ""}
  2436. <div class="consent-actions">
  2437. ${awaiting ? `
  2438. <button class="button button--primary" type="button" data-action="confirm-command">\u786E\u8BA4\u6302\u53F7</button>
  2439. <button class="button button--secondary" type="button" data-action="reject-command">\u6682\u4E0D\u64CD\u4F5C</button>
  2440. ` : ""}
  2441. ${canRequery ? `
  2442. <button class="button button--secondary" type="button" data-action="requery-registration">\u91CD\u65B0\u67E5\u8BE2\u53F7\u6E90</button>
  2443. ` : ""}
  2444. ${canClose ? `
  2445. <button class="button button--primary" type="button" data-action="close-command">${command.status === "SUCCEEDED" ? "\u67E5\u770B\u9996\u9875\u4E8B\u9879" : "\u5173\u95ED"}</button>
  2446. ` : ""}
  2447. </div>
  2448. </section>
  2449. </div>
  2450. `;
  2451. };
  2452. var renderReportInterpreting = () => `
  2453. <section class="report-view report-loading" aria-labelledby="loadingTitle" aria-busy="true">
  2454. <button class="text-button report-view__back" type="button" data-action="go-home">
  2455. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  2456. </button>
  2457. <div class="loading-orbit" aria-hidden="true"><i data-lucide="sparkles"></i></div>
  2458. <p class="section-eyebrow">AI \u8F85\u52A9\u89E3\u91CA</p>
  2459. <h1 id="loadingTitle">\u6B63\u5728\u6574\u7406\u62A5\u544A\u89E3\u8BFB</h1>
  2460. <p>\u6B63\u5728\u7ED3\u5408\u53C2\u8003\u8303\u56F4\u6574\u7406\u4FBF\u4E8E\u7406\u89E3\u7684\u4FE1\u606F\uFF0C\u8BF7\u7A0D\u5019\u3002\u7ED3\u679C\u4EC5\u4F9B\u8F85\u52A9\u7406\u89E3\uFF0C\u4E0D\u4EE3\u8868\u8BCA\u65AD\u7ED3\u8BBA\u3002</p>
  2461. <div class="loading-progress" role="progressbar" aria-label="\u62A5\u544A\u89E3\u8BFB\u5904\u7406\u4E2D">
  2462. <span></span>
  2463. </div>
  2464. <div class="loading-step"><i data-lucide="loader-circle"></i>\u5206\u6790\u4ECD\u5728\u8FDB\u884C\uFF0C\u6682\u4E0D\u63D0\u4F9B\u786E\u5B9A\u6027\u7ED3\u8BBA</div>
  2465. </section>
  2466. `;
  2467. var reportUnavailablePresentation = Object.freeze({
  2468. OCR_REVIEW_REQUIRED: Object.freeze({
  2469. title: "\u5F53\u524D\u62A5\u544A\u6682\u4E0D\u9002\u5408\u81EA\u52A8\u89E3\u8BFB",
  2470. description: "\u62A5\u544A\u5185\u5BB9\u9700\u8981\u4EBA\u5DE5\u6838\u5BF9\uFF0C\u60A8\u4ECD\u53EF\u67E5\u770B\u533B\u9662\u539F\u59CB\u62A5\u544A\u3002",
  2471. actions: Object.freeze([
  2472. Object.freeze({
  2473. action: "view-original-report",
  2474. label: "\u67E5\u770B\u539F\u59CB\u62A5\u544A",
  2475. tone: "primary"
  2476. }),
  2477. Object.freeze({
  2478. action: "contact-human-service",
  2479. label: "\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1",
  2480. tone: "secondary"
  2481. })
  2482. ])
  2483. }),
  2484. CRITICAL_NOTICE: Object.freeze({
  2485. title: "\u62A5\u544A\u542B\u533B\u9662\u5371\u6025\u63D0\u793A",
  2486. description: "\u8BF7\u7ACB\u5373\u6309\u533B\u9662\u901A\u77E5\u8054\u7CFB\u533B\u52A1\u4EBA\u5458\uFF0C\u5E76\u4F18\u5148\u9075\u5FAA\u533B\u9662\u65E2\u6709\u5904\u7F6E\u6D41\u7A0B\u3002",
  2487. actions: Object.freeze([
  2488. Object.freeze({
  2489. action: "contact-human-service",
  2490. label: "\u7ACB\u5373\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1",
  2491. tone: "primary"
  2492. }),
  2493. Object.freeze({
  2494. action: "view-original-report",
  2495. label: "\u67E5\u770B\u539F\u59CB\u62A5\u544A",
  2496. tone: "secondary"
  2497. })
  2498. ])
  2499. }),
  2500. AI_UNAVAILABLE: Object.freeze({
  2501. title: "AI\u8F85\u52A9\u89E3\u8BFB\u6682\u4E0D\u53EF\u7528",
  2502. description: "\u533B\u9662\u539F\u59CB\u62A5\u544A\u4ECD\u53EF\u6B63\u5E38\u67E5\u770B\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
  2503. }),
  2504. REPORT_INVALIDATED: Object.freeze({
  2505. title: "\u533B\u9662\u5DF2\u66F4\u65B0\u6216\u64A4\u56DE\u62A5\u544A",
  2506. description: "\u539FAI\u8F85\u52A9\u89E3\u91CA\u5DF2\u5931\u6548\u3002\u6700\u65B0\u7248\u672C\u5C1A\u672A\u540C\u6B65\u5B8C\u6210\uFF0C\u8BF7\u8FD4\u56DE\u62A5\u544A\u4E2D\u5FC3\u67E5\u770B\u72B6\u6001\u3002",
  2507. actions: Object.freeze([
  2508. Object.freeze({
  2509. action: "open-report-center",
  2510. label: "\u67E5\u770B\u62A5\u544A\u72B6\u6001",
  2511. tone: "primary"
  2512. }),
  2513. Object.freeze({
  2514. action: "contact-human-service",
  2515. label: "\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1",
  2516. tone: "secondary"
  2517. })
  2518. ])
  2519. }),
  2520. UNSUPPORTED: Object.freeze({
  2521. title: "\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u89E3\u8BFB",
  2522. description: "\u60A8\u53EF\u4EE5\u67E5\u770B\u539F\u59CB\u62A5\u544A\u6216\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1\u3002"
  2523. })
  2524. });
  2525. var renderReportUnavailable = ({
  2526. reason,
  2527. reportId,
  2528. serviceNotice
  2529. } = {}) => {
  2530. const presentation = Object.hasOwn(reportUnavailablePresentation, reason) ? reportUnavailablePresentation[reason] : reportUnavailablePresentation.UNSUPPORTED;
  2531. const safeReportId = typeof reportId === "string" && /^[A-Za-z0-9._-]+$/.test(reportId) ? reportId : "";
  2532. const actions = presentation.actions ?? [
  2533. {
  2534. action: "view-original-report",
  2535. label: presentation.primaryActionLabel ?? "\u67E5\u770B\u539F\u59CB\u62A5\u544A",
  2536. tone: "primary"
  2537. },
  2538. {
  2539. action: "contact-human-service",
  2540. label: "\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1",
  2541. tone: "secondary"
  2542. }
  2543. ];
  2544. return `
  2545. <section class="report-view report-unavailable" role="status" aria-labelledby="reportUnavailableTitle">
  2546. <div class="report-unavailable__icon" aria-hidden="true"><i data-lucide="shield-alert"></i></div>
  2547. <h1 id="reportUnavailableTitle">${escapeHtml(presentation.title)}</h1>
  2548. <p>${escapeHtml(presentation.description)}</p>
  2549. ${typeof serviceNotice === "string" && serviceNotice ? `
  2550. <p class="result-service-notice" role="status">${escapeHtml(serviceNotice)}</p>
  2551. ` : ""}
  2552. <div class="report-unavailable__actions">
  2553. ${actions.map(({ action, label, tone }) => `
  2554. <button class="button button--${tone}" type="button" data-action="${escapeHtml(action)}" data-report-id="${escapeHtml(safeReportId)}">${escapeHtml(label)}</button>
  2555. `).join("")}
  2556. </div>
  2557. </section>
  2558. `;
  2559. };
  2560. var renderReportServiceOverlay = (state = {}) => {
  2561. const reason = state?.overlays?.reportUnavailableReason;
  2562. const presentation = Object.hasOwn(reportUnavailablePresentation, reason) ? reportUnavailablePresentation[reason] : reportUnavailablePresentation.UNSUPPORTED;
  2563. return `
  2564. <div class="consent-backdrop report-service-backdrop" role="presentation">
  2565. <section class="consent-sheet report-service-sheet" role="dialog" aria-modal="true" aria-labelledby="reportServiceTitle" tabindex="-1">
  2566. <div class="consent-sheet__icon"><i data-lucide="headphones"></i></div>
  2567. <p class="section-eyebrow">\u4EBA\u5DE5\u534F\u52A9</p>
  2568. <h2 id="reportServiceTitle">\u4EBA\u5DE5\u670D\u52A1\u7A97\u53E3</h2>
  2569. <p>${escapeHtml(presentation.title)}</p>
  2570. <p>${escapeHtml(presentation.description)}</p>
  2571. <div class="consent-note">
  2572. <p>\u533B\u9662\u670D\u52A1\u4EBA\u5458\u53EF\u534F\u52A9\u6838\u5BF9\u62A5\u544A\u72B6\u6001\u548C\u539F\u59CB\u5185\u5BB9\u3002</p>
  2573. <p>\u672C\u539F\u578B\u4EC5\u6F14\u793A\u670D\u52A1\u5165\u53E3\uFF0C\u4E0D\u4F1A\u771F\u5B9E\u53D1\u8D77\u7535\u8BDD\u6216\u5728\u7EBF\u4F1A\u8BDD\u3002</p>
  2574. </div>
  2575. <div class="consent-actions">
  2576. <button class="button button--primary" type="button" data-action="close-report-service">\u77E5\u9053\u4E86</button>
  2577. </div>
  2578. </section>
  2579. </div>
  2580. `;
  2581. };
  2582. var renderReportInterpretationResult = (result, options = {}) => {
  2583. const interpretation = result?.interpretation;
  2584. if (!interpretation || typeof interpretation !== "object" || Array.isArray(interpretation)) {
  2585. return "";
  2586. }
  2587. const highlights = Array.isArray(interpretation.highlights) ? interpretation.highlights : [];
  2588. const explanations = Array.isArray(interpretation.explanations) ? interpretation.explanations : [];
  2589. const commonFactors = Array.isArray(interpretation.commonFactors) ? interpretation.commonFactors : explanations.map((item) => item?.commonFactors).filter(Boolean);
  2590. const canDo = Array.isArray(interpretation.canDo) ? interpretation.canDo : [];
  2591. const evidence = Array.isArray(interpretation.evidence) ? interpretation.evidence : [];
  2592. const reportId = typeof result?.reportId === "string" ? result.reportId : "";
  2593. const reportAction = (action) => `data-action="${action}" data-report-id="${escapeHtml(reportId)}"`;
  2594. const serviceNotice = typeof options?.serviceNotice === "string" ? options.serviceNotice : "";
  2595. const feedbackCategories = [
  2596. ["HELPFUL", "\u6709\u5E2E\u52A9"],
  2597. ["METRIC_INACCURATE", "\u6307\u6807\u89E3\u91CA\u4E0D\u51C6\u786E"],
  2598. ["HARD_TO_UNDERSTAND", "\u5185\u5BB9\u96BE\u7406\u89E3"],
  2599. ["DIFFERS_FROM_DOCTOR", "\u4E0E\u533B\u751F\u8BF4\u660E\u4E0D\u4E00\u81F4"],
  2600. ["OTHER", "\u5176\u4ED6\u95EE\u9898"]
  2601. ];
  2602. return `
  2603. <section class="report-view result-view" aria-labelledby="resultTitle">
  2604. <button class="text-button report-view__back" type="button" data-action="go-home">
  2605. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  2606. </button>
  2607. <div class="result-summary">
  2608. <p class="section-eyebrow">AI \u8F85\u52A9\u89E3\u91CA \xB7 \u7ED3\u679C\u7248\u672C ${escapeHtml(result.resultVersion)}</p>
  2609. <h2>\u7ED3\u679C\u6982\u62EC</h2>
  2610. <h1 id="resultTitle">${escapeHtml(interpretation.headline)}</h1>
  2611. <p>${escapeHtml(interpretation.summary)}</p>
  2612. </div>
  2613. <div class="safety-callout">
  2614. <strong><i data-lucide="shield-check"></i>\u5B89\u5168\u63D0\u793A</strong>
  2615. <p>${escapeHtml(interpretation.safety)}</p>
  2616. </div>
  2617. <section class="result-card">
  2618. <h2>\u9700\u8981\u5173\u6CE8</h2>
  2619. <ul>${highlights.map((item) => `
  2620. <li>
  2621. <span>${escapeHtml(item?.name)}</span>
  2622. <strong>${escapeHtml(item?.value)}</strong>
  2623. <em>${escapeHtml(item?.flag)}</em>
  2624. </li>
  2625. `).join("")}</ul>
  2626. </section>
  2627. <section class="result-card">
  2628. <h2>\u6307\u6807\u89E3\u91CA</h2>
  2629. ${explanations.map((item) => `
  2630. <article class="explanation-item">
  2631. <h3>${escapeHtml(item.indicator)}</h3>
  2632. <p>${escapeHtml(item.explanation)}</p>
  2633. </article>
  2634. `).join("")}
  2635. </section>
  2636. <section class="result-card">
  2637. <h2>\u5E38\u89C1\u76F8\u5173\u56E0\u7D20</h2>
  2638. <ul>${commonFactors.map((factor) => `<li>${escapeHtml(factor)}</li>`).join("")}</ul>
  2639. <p class="result-factors-note">\u4E0A\u8FF0\u56E0\u7D20\u53EA\u662F\u5E2E\u52A9\u7406\u89E3\u6307\u6807\u7684\u5E38\u89C1\u60C5\u51B5\uFF0C\u4E0D\u4EE3\u8868\u4F60\u60A3\u6709\u76F8\u5E94\u75BE\u75C5\uFF0C\u4E5F\u4E0D\u80FD\u4F5C\u4E3A\u8BCA\u65AD\u7ED3\u679C\u3002</p>
  2640. </section>
  2641. <section class="result-card">
  2642. <h2>\u53EF\u4EE5\u600E\u4E48\u505A</h2>
  2643. <ul>${canDo.map((action) => `<li>${escapeHtml(action)}</li>`).join("")}</ul>
  2644. </section>
  2645. <section class="result-card result-evidence">
  2646. <h2>\u5185\u5BB9\u4F9D\u636E</h2>
  2647. <ul>${evidence.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
  2648. <p>\u7ED3\u679C\u7248\u672C ${escapeHtml(result.resultVersion)} \xB7 \u5BF9\u5E94\u5F53\u524D\u533B\u9662\u539F\u59CB\u62A5\u544A</p>
  2649. </section>
  2650. ${reportId ? `
  2651. <section class="result-card result-operations" aria-labelledby="resultOperationsTitle">
  2652. <h2 id="resultOperationsTitle">\u9700\u8981\u8FDB\u4E00\u6B65\u5E2E\u52A9</h2>
  2653. ${serviceNotice ? `
  2654. <p class="result-service-notice" role="status">${escapeHtml(serviceNotice)}</p>
  2655. ` : ""}
  2656. <div class="result-action-grid">
  2657. <button class="button button--secondary" type="button" ${reportAction("view-original-report")}>\u67E5\u770B\u539F\u59CB\u62A5\u544A</button>
  2658. <button class="button button--secondary" type="button" ${reportAction("consult-ordering-doctor")}>\u54A8\u8BE2\u5F00\u5355\u533B\u751F</button>
  2659. <button class="button button--secondary" type="button" ${reportAction("online-consultation")}>\u5728\u7EBF\u54A8\u8BE2</button>
  2660. <button class="button button--secondary" type="button" ${reportAction("contact-human-service")}>\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1</button>
  2661. <button class="button button--secondary" type="button" ${reportAction("open-report-feedback")}>\u63D0\u4EA4\u53CD\u9988</button>
  2662. <button class="button button--secondary result-revoke" type="button" ${reportAction("revoke-report-consent")}>\u64A4\u56DE\u6388\u6743</button>
  2663. </div>
  2664. </section>
  2665. ` : ""}
  2666. ${reportId && options?.feedbackOpen ? `
  2667. <section class="result-card report-feedback" aria-labelledby="reportFeedbackTitle">
  2668. <h2 id="reportFeedbackTitle">\u8FD9\u6B21\u89E3\u91CA\u5BF9\u4F60\u6709\u5E2E\u52A9\u5417\uFF1F</h2>
  2669. <p>\u8BF7\u9009\u62E9\u6700\u7B26\u5408\u7684\u4E00\u9879\uFF0C\u53CD\u9988\u4EC5\u7528\u4E8E\u6539\u8FDB\u672C\u6B21\u8F85\u52A9\u89E3\u91CA\u3002</p>
  2670. <div class="feedback-options">
  2671. ${feedbackCategories.map(([category, label]) => `
  2672. <button
  2673. class="button button--secondary"
  2674. type="button"
  2675. data-action="submit-report-feedback"
  2676. data-report-id="${escapeHtml(reportId)}"
  2677. data-filter="${category}"
  2678. >${label}</button>
  2679. `).join("")}
  2680. </div>
  2681. </section>
  2682. ` : ""}
  2683. ${options?.feedbackSubmitted ? `
  2684. <p class="report-feedback-confirmation" role="status">\u611F\u8C22\u53CD\u9988\uFF0C\u5DF2\u8BB0\u5F55\u4F60\u7684\u9009\u62E9\u3002</p>
  2685. ` : ""}
  2686. </section>
  2687. `;
  2688. };
  2689. var renderQueueStatus = (state, appointmentId) => {
  2690. const queue = (Array.isArray(state?.patientWorkItems) ? state.patientWorkItems : []).find((item) => item?.type === "WAITING_QUEUE" && item.appointmentId === appointmentId);
  2691. if (!queue) {
  2692. return `
  2693. <section class="report-view queue-view" aria-labelledby="queueTitle">
  2694. <button class="text-button report-view__back" type="button" data-action="go-home">
  2695. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  2696. </button>
  2697. <div class="empty-card">
  2698. <i data-lucide="clock-3"></i>
  2699. <div>
  2700. <h1 id="queueTitle">\u5019\u8BCA\u4FE1\u606F\u5F85\u66F4\u65B0</h1>
  2701. <p>\u5B8C\u6210\u7B7E\u5230\u540E\uFF0C\u6392\u961F\u53F7\u548C\u9884\u8BA1\u65F6\u95F4\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002</p>
  2702. </div>
  2703. </div>
  2704. </section>
  2705. `;
  2706. }
  2707. const updatedAt = typeof queue.updatedAt === "string" ? queue.updatedAt.slice(11, 16) : "\u5F85\u66F4\u65B0";
  2708. return `
  2709. <section class="report-view queue-view" aria-labelledby="queueTitle">
  2710. <button class="text-button report-view__back" type="button" data-action="go-home">
  2711. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  2712. </button>
  2713. <div class="queue-hero">
  2714. <p class="section-eyebrow">\u795E\u7ECF\u5185\u79D1 \xB7 \u5B9E\u65F6\u5019\u8BCA</p>
  2715. <h1 id="queueTitle">\u6392\u961F\u53F7 ${escapeHtml(queue.queueNumber ?? "\u5F85\u66F4\u65B0")}</h1>
  2716. <p>${escapeHtml(queue.room ?? "\u8BCA\u5BA4\u5F85\u5206\u914D")}</p>
  2717. </div>
  2718. ${queue.delayed ? `
  2719. <div class="queue-delay" role="status">
  2720. <i data-lucide="triangle-alert"></i>
  2721. <span>\u5019\u8BCA\u65F6\u95F4\u53EF\u80FD\u5EF6\u540E\uFF0C\u8BF7\u7559\u610F\u53EB\u53F7\u66F4\u65B0\u3002</span>
  2722. </div>
  2723. ` : ""}
  2724. <div class="queue-metrics">
  2725. <article><span>\u524D\u65B9\u4EBA\u6570</span><strong>\u524D\u65B9 ${escapeHtml(queue.peopleAhead ?? 0)} \u4EBA</strong></article>
  2726. <article><span>\u9884\u8BA1\u65F6\u95F4</span><strong>\u9884\u8BA1\u7B49\u5F85 ${escapeHtml(queue.estimatedMinutes ?? 0)} \u5206\u949F</strong></article>
  2727. </div>
  2728. <div class="queue-detail">
  2729. <p><i data-lucide="door-open"></i><span>\u5F53\u524D\u8BCA\u5BA4</span><strong>${escapeHtml(queue.room ?? "\u5F85\u5206\u914D")}</strong></p>
  2730. <p><i data-lucide="refresh-cw"></i><span>\u961F\u5217\u66F4\u65B0\u65F6\u95F4</span><strong>${escapeHtml(updatedAt)}</strong></p>
  2731. </div>
  2732. <button class="button button--secondary queue-route" type="button" data-action="open-route" data-appointment-id="${escapeHtml(appointmentId)}">
  2733. <i data-lucide="navigation"></i>\u9662\u5185\u5BFC\u822A
  2734. </button>
  2735. </section>
  2736. `;
  2737. };
  2738. var renderRouteView = (location) => `
  2739. <section class="report-view route-view" aria-labelledby="routeTitle">
  2740. <button class="text-button report-view__back" type="button" data-action="go-home">
  2741. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  2742. </button>
  2743. <div class="route-map" aria-hidden="true">
  2744. <i data-lucide="map-pinned"></i>
  2745. <span></span><span></span><span></span>
  2746. </div>
  2747. <div class="module-header">
  2748. <p class="section-eyebrow">\u9662\u5185\u8DEF\u7EBF</p>
  2749. <h1 id="routeTitle">${escapeHtml(location ?? "\u95E8\u8BCA\u4E09\u697C\u795E\u7ECF\u5185\u79D1\u8BCA\u533A")}</h1>
  2750. <p>\u4ECE\u95E8\u8BCA\u5927\u5385\u4E58 2 \u53F7\u7535\u68AF\u81F3\u4E09\u697C\uFF0C\u51FA\u7535\u68AF\u540E\u53F3\u8F6C\u7EA6 30 \u7C73\u3002</p>
  2751. </div>
  2752. <div class="queue-detail">
  2753. <p><i data-lucide="footprints"></i><span>\u9884\u8BA1\u6B65\u884C</span><strong>4 \u5206\u949F</strong></p>
  2754. <p><i data-lucide="accessibility"></i><span>\u65E0\u969C\u788D\u8DEF\u7EBF</span><strong>\u53EF\u7528</strong></p>
  2755. </div>
  2756. </section>
  2757. `;
  2758. var pageHeader = (eyebrow, title, description) => `
  2759. <div class="module-header">
  2760. <p class="section-eyebrow">${eyebrow}</p>
  2761. <h1>${title}</h1>
  2762. <p>${description}</p>
  2763. </div>
  2764. `;
  2765. var careGroupDefinitions = [
  2766. {
  2767. title: "\u5F85\u529E\u7406",
  2768. statuses: ["ACTION_REQUIRED", "UPCOMING"],
  2769. fallback: "\u95E8\u8BCA\u8D39\u7528\u5F85\u786E\u8BA4"
  2770. },
  2771. {
  2772. title: "\u8FDB\u884C\u4E2D",
  2773. statuses: ["IN_PROGRESS"],
  2774. fallback: "\u4ECA\u65E5\u5C31\u533B\u65C5\u7A0B"
  2775. },
  2776. {
  2777. title: "\u5DF2\u5B8C\u6210",
  2778. statuses: ["COMPLETED"],
  2779. fallback: "\u8EAB\u4EFD\u4FE1\u606F\u5DF2\u6838\u9A8C"
  2780. },
  2781. {
  2782. title: "\u9700\u8981\u4EBA\u5DE5\u5904\u7406",
  2783. statuses: ["MANUAL_REQUIRED"],
  2784. fallback: "\u4EBA\u5DE5\u670D\u52A1\u968F\u65F6\u53EF\u7528"
  2785. }
  2786. ];
  2787. var renderCareOverview = (state) => {
  2788. const items = Array.isArray(state?.patientWorkItems) ? state.patientWorkItems.filter((item) => item && typeof item === "object") : [];
  2789. return `
  2790. <section class="module-page" aria-labelledby="careOverviewTitle">
  2791. ${pageHeader("\u5C31\u533B\u603B\u89C8", "\u6211\u7684\u5C31\u533B", "\u6309\u529E\u7406\u72B6\u6001\u96C6\u4E2D\u67E5\u770B\u95E8\u8BCA\u3001\u68C0\u67E5\u548C\u8BCA\u540E\u4E8B\u9879\u3002")}
  2792. <div class="care-groups">
  2793. ${careGroupDefinitions.map((group) => {
  2794. const matches = items.filter((item) => group.statuses.includes(item.status));
  2795. const visible = matches.length > 0 ? matches : [{ title: group.fallback, description: "\u8FDB\u5165\u8BE6\u60C5\u67E5\u770B\u4E0B\u4E00\u6B65" }];
  2796. return `
  2797. <section class="care-group">
  2798. <div class="section-heading">
  2799. <h2>${group.title}</h2>
  2800. <span>${visible.length}</span>
  2801. </div>
  2802. ${visible.slice(0, 3).map((item) => `
  2803. <article class="compact-row">
  2804. <span class="compact-row__icon"><i data-lucide="clipboard-check"></i></span>
  2805. <div>
  2806. <strong>${escapeHtml(item.title)}</strong>
  2807. <p>${escapeHtml(item.description ?? "\u72B6\u6001\u548C\u53EF\u7528\u64CD\u4F5C\u6301\u7EED\u66F4\u65B0")}</p>
  2808. </div>
  2809. <i data-lucide="chevron-right"></i>
  2810. </article>
  2811. `).join("")}
  2812. </section>
  2813. `;
  2814. }).join("")}
  2815. </div>
  2816. </section>
  2817. `;
  2818. };
  2819. var recordEntries = [
  2820. { id: "lab-reports", label: "\u68C0\u9A8C\u62A5\u544A", meta: "\u8840\u5E38\u89C4 \xB7 3 \u9879\u5F02\u5E38", filter: "abnormal", icon: "flask-conical", action: "open-report-center", reportFamily: "LAB" },
  2821. { id: "exam-reports", label: "\u68C0\u67E5\u62A5\u544A", meta: "\u5F71\u50CF\u4E0E\u8D85\u58F0\u62A5\u544A", filter: "all", icon: "scan-line", action: "open-report-center", reportFamily: "EXAM" },
  2822. { id: "visits", label: "\u95E8\u8BCA\u8BB0\u5F55", meta: "\u5386\u6B21\u63A5\u8BCA\u4E0E\u8BCA\u540E\u8BB0\u5F55", filter: "all", icon: "notebook-tabs" },
  2823. { id: "medication", label: "\u5904\u65B9\u4E0E\u7528\u836F", meta: "\u5904\u65B9\u3001\u7528\u836F\u63D0\u9192", filter: "all", icon: "pill" },
  2824. { id: "allergies", label: "\u8FC7\u654F\u53F2", meta: "\u6682\u672A\u8BB0\u5F55\u836F\u7269\u8FC7\u654F", filter: "all", icon: "shield-alert" },
  2825. { id: "consents", label: "\u6388\u6743\u8BB0\u5F55", meta: "\u67E5\u770B\u548C\u64A4\u56DE\u6570\u636E\u6388\u6743", filter: "unread", icon: "file-lock-2" }
  2826. ];
  2827. var renderRecords = (state) => {
  2828. const activeFilter = ["all", "abnormal", "unread"].includes(
  2829. state?.recordsFilter
  2830. ) ? state.recordsFilter : "all";
  2831. const visible = activeFilter === "all" ? recordEntries : recordEntries.filter((entry) => entry.filter === activeFilter);
  2832. return `
  2833. <section class="module-page" aria-labelledby="recordsTitle">
  2834. ${pageHeader("\u5065\u5EB7\u6863\u6848", "\u6211\u7684\u5065\u5EB7\u8BB0\u5F55", "\u62A5\u544A\u3001\u95E8\u8BCA\u8BB0\u5F55\u548C\u6388\u6743\u4FE1\u606F\u7EDF\u4E00\u5F52\u6863\u3002")}
  2835. <div class="filter-tabs" role="group" aria-label="\u62A5\u544A\u7B5B\u9009">
  2836. ${[
  2837. ["all", "\u5168\u90E8"],
  2838. ["abnormal", "\u5F02\u5E38"],
  2839. ["unread", "\u672A\u89E3\u8BFB"]
  2840. ].map(([id, label]) => `
  2841. <button type="button" data-action="set-record-filter" data-filter="${id}" aria-pressed="${activeFilter === id}">
  2842. ${label}
  2843. </button>
  2844. `).join("")}
  2845. </div>
  2846. <div class="module-list">
  2847. ${visible.map((entry) => `
  2848. <${entry.action ? "button" : "article"} class="compact-row"${entry.action ? ` type="button" data-action="${entry.action}" data-filter="${entry.reportFamily}"` : ""}>
  2849. <span class="compact-row__icon"><i data-lucide="${entry.icon}"></i></span>
  2850. <div><strong>${entry.label}</strong><p>${entry.meta}</p></div>
  2851. <i data-lucide="chevron-right"></i>
  2852. </${entry.action ? "button" : "article"}>
  2853. `).join("")}
  2854. </div>
  2855. </section>
  2856. `;
  2857. };
  2858. var profileEntries = [
  2859. ["\u5C31\u8BCA\u4EBA\u7BA1\u7406", "users-round"],
  2860. ["\u6388\u6743\u4E2D\u5FC3", "shield-check"],
  2861. ["\u6D88\u606F\u8BBE\u7F6E", "bell-ring"],
  2862. ["\u5E38\u7528\u9662\u533A", "hospital"],
  2863. ["\u533B\u4FDD\u548C\u652F\u4ED8", "badge-cent"],
  2864. ["\u7535\u5B50\u7968\u636E", "receipt-text"],
  2865. ["\u4EBA\u5DE5\u5BA2\u670D", "headphones"],
  2866. ["\u5173\u4E8E AI", "sparkles"]
  2867. ];
  2868. var renderProfile = () => `
  2869. <section class="module-page" aria-labelledby="profileTitle">
  2870. ${pageHeader("\u4E2A\u4EBA\u4E2D\u5FC3", "\u6211\u7684", "\u7BA1\u7406\u5C31\u8BCA\u4EBA\u3001\u9690\u79C1\u6388\u6743\u548C\u5E38\u7528\u670D\u52A1\u8BBE\u7F6E\u3002")}
  2871. <div class="module-list">
  2872. ${profileEntries.map(([label, icon]) => `
  2873. <button class="compact-row compact-row--button" type="button">
  2874. <span class="compact-row__icon"><i data-lucide="${icon}"></i></span>
  2875. <strong>${label}</strong>
  2876. <i data-lucide="chevron-right"></i>
  2877. </button>
  2878. `).join("")}
  2879. </div>
  2880. </section>
  2881. `;
  2882. var messageCategories = ["\u62A5\u544A", "\u5C31\u533B", "\u7F34\u8D39", "\u968F\u8BBF", "\u7CFB\u7EDF"];
  2883. var renderMessages = (state) => {
  2884. const messages = Array.isArray(state?.messages) ? state.messages.filter((message) => message && typeof message === "object") : [];
  2885. return `
  2886. <section class="module-page" aria-labelledby="messagesTitle">
  2887. ${pageHeader("\u6D88\u606F\u4E2D\u5FC3", "\u6D88\u606F", "\u6309\u670D\u52A1\u7C7B\u578B\u5B9A\u4F4D\u5230\u5BF9\u5E94\u5C31\u533B\u4E8B\u9879\uFF0C\u4E0D\u4F1A\u76F4\u63A5\u6267\u884C\u529E\u7406\u64CD\u4F5C\u3002")}
  2888. <div class="message-categories" aria-label="\u6D88\u606F\u7C7B\u578B">
  2889. ${messageCategories.map((category) => `<span>${category}</span>`).join("")}
  2890. </div>
  2891. <div class="module-list">
  2892. ${messages.length > 0 ? messages.map((message) => `
  2893. <button class="compact-row compact-row--button" type="button" data-action="open-message" data-message-id="${escapeHtml(message.id)}">
  2894. <span class="compact-row__icon"><i data-lucide="${message.type === "REPORT" ? "file-chart-column" : "bell"}"></i></span>
  2895. <div>
  2896. <strong>${escapeHtml(message.title ?? message.text ?? "\u670D\u52A1\u6D88\u606F")}</strong>
  2897. <p>${escapeHtml(message.createdAt ?? "\u521A\u521A\u66F4\u65B0")}</p>
  2898. </div>
  2899. <i data-lucide="chevron-right"></i>
  2900. </button>
  2901. `).join("") : `
  2902. <div class="empty-card">
  2903. <i data-lucide="inbox"></i>
  2904. <div><strong>\u6682\u65E0\u65B0\u6D88\u606F</strong><p>\u65B0\u7684\u62A5\u544A\u548C\u5C31\u533B\u63D0\u9192\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002</p></div>
  2905. </div>
  2906. `}
  2907. </div>
  2908. </section>
  2909. `;
  2910. };
  2911. var renderSystemState = (type, state) => {
  2912. const cachedTaskCount = Array.isArray(state?.patientWorkItems) ? state.patientWorkItems.length : 0;
  2913. const definitions = {
  2914. INITIAL_LOADING: ["\u6B63\u5728\u52A0\u8F7D\u60A3\u8005\u670D\u52A1\u53F0", "\u6B63\u5728\u540C\u6B65\u6700\u65B0\u5C31\u533B\u4E8B\u9879\uFF0C\u8BF7\u7A0D\u5019\u3002", "loader-circle"],
  2915. EMPTY: ["\u6682\u65E0\u76F8\u5173\u5185\u5BB9", "\u53EF\u4EE5\u8FD4\u56DE\u9996\u9875\u6216\u5C1D\u8BD5\u5176\u4ED6\u670D\u52A1\u3002", "inbox"],
  2916. OFFLINE: ["\u7F51\u7EDC\u8FDE\u63A5\u4E0D\u53EF\u7528", `\u5DF2\u4FDD\u7559 ${cachedTaskCount} \u9879\u672C\u5730\u4EFB\u52A1\uFF0C\u8054\u7F51\u540E\u5C06\u81EA\u52A8\u66F4\u65B0\u3002`, "wifi-off"],
  2917. DEGRADED: ["\u90E8\u5206\u667A\u80FD\u670D\u52A1\u6682\u4E0D\u53EF\u7528", "\u57FA\u7840\u5C31\u533B\u4E8B\u9879\u4ECD\u53EF\u67E5\u770B\uFF0C\u4E5F\u53EF\u4EE5\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1\u3002", "shield-alert"],
  2918. QUERY_FAILED: ["\u67E5\u8BE2\u5931\u8D25", "\u8BF7\u7A0D\u540E\u91CD\u65B0\u67E5\u8BE2\uFF0C\u65E7\u7ED3\u679C\u4E0D\u4F1A\u7528\u4E8E\u7EE7\u7EED\u529E\u7406\u3002", "search-x"],
  2919. COMMAND_FAILED: ["\u529E\u7406\u5931\u8D25", "\u672C\u6B21\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u6216\u8054\u7CFB\u4EBA\u5DE5\u670D\u52A1\u3002", "circle-x"],
  2920. COMMAND_UNKNOWN: ["\u7ED3\u679C\u786E\u8BA4\u4E2D\uFF0C\u8BF7\u52FF\u91CD\u590D\u64CD\u4F5C", "\u6B63\u5728\u6838\u9A8C\u533B\u9662\u7CFB\u7EDF\u7ED3\u679C\uFF0C\u53EF\u4EE5\u7A0D\u540E\u67E5\u770B\u72B6\u6001\u3002", "circle-ellipsis"]
  2921. };
  2922. const [title, description, icon] = definitions[type] ?? definitions.EMPTY;
  2923. return `
  2924. <section class="system-state" role="status">
  2925. <span><i data-lucide="${icon}"></i></span>
  2926. <h1>${title}</h1>
  2927. <p>${description}</p>
  2928. <button class="button button--secondary" type="button" data-action="go-home">\u8FD4\u56DE\u9996\u9875</button>
  2929. </section>
  2930. `;
  2931. };
  2932. var renderScenarioPanel = (state) => {
  2933. const activeScenario = demoScenarios.includes(state?.demoScenario) ? state.demoScenario : "EMPTY_HOME";
  2934. const activeTask = state?.activeAgentTaskProjection;
  2935. const commands = Object.values(state?.commandProjections ?? {});
  2936. const latestFixture = state?.appliedProjectionFixtureIds?.at?.(-1) ?? "\u65E0";
  2937. const renderScenarioGroup = (groupId, title, scenarios) => `
  2938. <section
  2939. class="scenario-group"
  2940. aria-labelledby="${groupId}"
  2941. >
  2942. <h2 id="${groupId}">${title}</h2>
  2943. <div class="scenario-list">
  2944. ${scenarios.map((scenario) => `
  2945. <button type="button" data-action="load-scenario" data-scenario="${scenario}" aria-pressed="${scenario === activeScenario}">
  2946. <span>${String(demoScenarios.indexOf(scenario) + 1).padStart(2, "0")}</span>
  2947. ${scenarioLabels[scenario]}
  2948. </button>
  2949. `).join("")}
  2950. </div>
  2951. </section>
  2952. `;
  2953. return `
  2954. <div class="scenario-controller">
  2955. <div class="scenario-controller__header">
  2956. <div>
  2957. <p>\u65B9\u6848\u6F14\u793A</p>
  2958. <strong>${escapeHtml(scenarioLabels[activeScenario])}</strong>
  2959. </div>
  2960. <button type="button" data-action="toggle-scenario-panel" aria-label="\u9690\u85CF\u6F14\u793A\u63A7\u5236\u5668">
  2961. <i data-lucide="panel-right-close"></i>
  2962. </button>
  2963. </div>
  2964. <div class="scenario-actions">
  2965. <button type="button" data-action="previous-scenario"><i data-lucide="arrow-left"></i>\u4E0A\u4E00\u6B65</button>
  2966. <button type="button" data-action="next-scenario">\u4E0B\u4E00\u6B65<i data-lucide="arrow-right"></i></button>
  2967. <button type="button" data-action="reset-scenario"><i data-lucide="rotate-ccw"></i>\u91CD\u7F6E</button>
  2968. </div>
  2969. <div class="scenario-groups" aria-label="\u6F14\u793A\u573A\u666F">
  2970. ${renderScenarioGroup(
  2971. "reportClosureScenarios",
  2972. "\u62A5\u544A\u95ED\u73AF",
  2973. demoScenarioGroups.REPORT_CLOSURE
  2974. )}
  2975. ${renderScenarioGroup(
  2976. "careClosureScenarios",
  2977. "\u5C31\u533B\u95ED\u73AF",
  2978. demoScenarioGroups.CARE_CLOSURE
  2979. )}
  2980. </div>
  2981. <div class="viewport-switcher" aria-label="\u6F14\u793A\u89C6\u53E3">
  2982. <button type="button" data-action="set-demo-viewport" data-viewport="390">390\xD7844</button>
  2983. <button type="button" data-action="set-demo-viewport" data-viewport="430">430\xD7932</button>
  2984. </div>
  2985. <dl class="projection-summary">
  2986. <div><dt>\u5F53\u524D fixture</dt><dd>${escapeHtml(latestFixture)}</dd></div>
  2987. <div><dt>Agent task</dt><dd>${escapeHtml(activeTask ? `${activeTask.type}/${activeTask.stage}` : "\u65E0")}</dd></div>
  2988. <div><dt>PatientWorkItem</dt><dd>${escapeHtml(state?.patientWorkItems?.length ?? 0)} \u9879</dd></div>
  2989. <div><dt>Command</dt><dd>${escapeHtml(commands.map((item) => `${item.commandId}/${item.status}`).join(", ") || "\u65E0")}</dd></div>
  2990. </dl>
  2991. <p class="scenario-shortcuts">\u2190/\u2192 \u5207\u6362 \xB7 R \u91CD\u7F6E \xB7 P \u663E\u793A/\u9690\u85CF</p>
  2992. </div>
  2993. `;
  2994. };
  2995. var renderCurrentView = (state) => {
  2996. const { reportId, reportView } = state?.overlays ?? {};
  2997. if (state?.systemState === "AI_INTERPRETING") {
  2998. return renderReportInterpreting();
  2999. }
  3000. if (state?.systemState) {
  3001. return renderSystemState(state.systemState, state);
  3002. }
  3003. if (state?.overlays?.journeyView === "CHECKIN_CONFIRMATION") {
  3004. return `
  3005. <section class="report-view checkin-view" aria-labelledby="checkinTitle">
  3006. <button class="text-button report-view__back" type="button" data-action="go-home">
  3007. <i data-lucide="arrow-left"></i>\u8FD4\u56DE\u9996\u9875
  3008. </button>
  3009. <div class="report-hero">
  3010. <p class="section-eyebrow">\u5230\u9662\u7B7E\u5230</p>
  3011. <h1 id="checkinTitle">\u8BF7\u786E\u8BA4\u672C\u4EBA\u5DF2\u5230\u8FBE\u8BCA\u533A</h1>
  3012. <p>\u7B7E\u5230\u9700\u8981\u60A3\u8005\u660E\u786E\u786E\u8BA4\uFF1B\u672C\u9875\u9762\u4E0D\u4F1A\u6839\u636E\u4F4D\u7F6E\u81EA\u52A8\u5B8C\u6210\u7B7E\u5230\u3002</p>
  3013. </div>
  3014. <div class="report-notice">
  3015. <i data-lucide="map-pin-check"></i>
  3016. <p>\u786E\u8BA4\u540E\u4ECD\u4EE5\u533B\u9662\u7B7E\u5230\u63A5\u53E3\u8FD4\u56DE\u7ED3\u679C\u4E3A\u51C6\u3002</p>
  3017. </div>
  3018. <div class="task-card__actions">
  3019. <button class="button button--primary" type="button" data-action="submit-checkin" data-appointment-id="${escapeHtml(state.overlays.appointmentId)}">
  3020. \u786E\u8BA4\u7B7E\u5230
  3021. </button>
  3022. <button class="button button--secondary" type="button" data-action="go-home">
  3023. \u6682\u4E0D\u7B7E\u5230
  3024. </button>
  3025. </div>
  3026. </section>
  3027. `;
  3028. }
  3029. if (state?.overlays?.journeyView === "ROUTE") {
  3030. const journey = state.journeyProjections?.find(
  3031. (item) => item?.appointmentId === state.overlays.appointmentId
  3032. );
  3033. const checkin = state.patientWorkItems?.find(
  3034. (item) => item?.type === "CHECK_IN" && item.appointmentId === state.overlays.appointmentId
  3035. );
  3036. return renderRouteView(
  3037. journey?.location ?? checkin?.location ?? journey?.room
  3038. );
  3039. }
  3040. if (state?.overlays?.journeyView === "QUEUE" && state?.overlays?.appointmentId) {
  3041. return renderQueueStatus(state, state.overlays.appointmentId);
  3042. }
  3043. if (reportView === "REPORT_DETAIL" && reportId) {
  3044. const report = state.reportSourceProjections?.[reportId];
  3045. return report?.family === "EXAM" ? renderExaminationReportDetail(report) : renderReportDetail(report, Boolean(state.overlays.reportExpanded));
  3046. }
  3047. if (reportView === "ORIGINAL_REPORT" && reportId) {
  3048. return renderOriginalReport(state.reportSourceProjections?.[reportId]);
  3049. }
  3050. if (reportView === "REPORT_UNAVAILABLE" && reportId) {
  3051. return renderReportUnavailable({
  3052. reason: state?.overlays?.reportUnavailableReason,
  3053. reportId,
  3054. serviceNotice: state?.overlays?.reportServiceNotice
  3055. });
  3056. }
  3057. if (reportView === "INTERPRETING" && reportId) {
  3058. return renderReportInterpreting();
  3059. }
  3060. if (reportView === "INTERPRETED" && hasValidReportConsent(state, reportId)) {
  3061. return renderReportInterpretationResult(
  3062. selectValidReportResult(state, reportId),
  3063. {
  3064. feedbackOpen: state?.overlays?.reportFeedbackOpen === true,
  3065. feedbackSubmitted: state?.overlays?.reportFeedbackSubmitted === true,
  3066. serviceNotice: state?.overlays?.reportServiceNotice
  3067. }
  3068. );
  3069. }
  3070. if (state?.activeTab === "assistant") {
  3071. return renderAssistant(state);
  3072. }
  3073. if (state?.activeTab === "care") {
  3074. return renderCareOverview(state);
  3075. }
  3076. if (state?.activeTab === "records") {
  3077. return state?.overlays?.reportView === "REPORT_CENTER" ? renderReportCenter(state) : renderRecords(state);
  3078. }
  3079. if (state?.activeTab === "profile") {
  3080. return renderProfile(state);
  3081. }
  3082. if (state?.activeTab === "messages") {
  3083. return renderMessages(state);
  3084. }
  3085. return renderHome(state);
  3086. };
  3087. var modalOverlayIdentity = null;
  3088. var modalOverlayDocument = null;
  3089. var consentFocusReturn = null;
  3090. var scenarioPanelVisible = true;
  3091. var demoViewport = "390";
  3092. var hasConsentDialog = (state) => typeof state?.overlays?.consentId === "string" && state.overlays.consentId.startsWith("consent-ai-");
  3093. var hasCommandDialog = (state) => Boolean(
  3094. state?.overlays?.commandId && state?.commandProjections?.[state.overlays.commandId]?.status === "AWAITING_CONFIRMATION"
  3095. );
  3096. var hasReportServiceDialog = (state) => state?.overlays?.reportView === "REPORT_UNAVAILABLE" && state?.overlays?.reportServiceNotice === "\u5DF2\u4E3A\u4F60\u6253\u5F00\u4EBA\u5DE5\u670D\u52A1\u5165\u53E3\u3002";
  3097. var focusModalDialog = (overlayRoot) => {
  3098. const dialog = overlayRoot?.querySelector?.('[role="dialog"]');
  3099. const firstButton = dialog?.querySelector?.("button");
  3100. (firstButton ?? dialog)?.focus?.();
  3101. };
  3102. var restoreConsentFocus = (root, elements) => {
  3103. const matchingTrigger = Array.from(
  3104. root?.querySelectorAll?.('[data-action="interpret-report"]') ?? []
  3105. ).find((trigger) => trigger.dataset?.reportId === consentFocusReturn?.reportId);
  3106. const fallback = elements.appView.querySelector?.(
  3107. "button, [href], [tabindex]"
  3108. ) ?? elements.appView;
  3109. (matchingTrigger ?? fallback)?.focus?.();
  3110. consentFocusReturn = null;
  3111. };
  3112. var renderAppShell = (state = createInitialState()) => {
  3113. const root = globalThis.document;
  3114. const elements = getShellElements(root);
  3115. if (!elements) {
  3116. return false;
  3117. }
  3118. if (root !== modalOverlayDocument) {
  3119. modalOverlayIdentity = null;
  3120. modalOverlayDocument = root;
  3121. }
  3122. elements.appHeader.innerHTML = renderHeader(state);
  3123. elements.appView.innerHTML = renderCurrentView(state);
  3124. elements.bottomNav.innerHTML = navItems.map(
  3125. ({ id, label, icon }) => `
  3126. <button
  3127. class="nav-item"
  3128. type="button"
  3129. data-nav-id="${id}"
  3130. data-action="navigate-tab"
  3131. data-tab-id="${id}"
  3132. aria-label="${label}"
  3133. ${state?.activeTab === id ? 'aria-current="page"' : ""}
  3134. >
  3135. <i data-lucide="${icon}"></i>
  3136. <span>${label}</span>
  3137. </button>
  3138. `
  3139. ).join("");
  3140. const scenarioPanel = root?.getElementById?.("scenarioPanel");
  3141. const phoneShell = root?.querySelector?.(".phone-shell");
  3142. if (scenarioPanel) {
  3143. scenarioPanel.innerHTML = renderScenarioPanel(state);
  3144. scenarioPanel.hidden = !scenarioPanelVisible;
  3145. }
  3146. if (phoneShell?.dataset) {
  3147. phoneShell.dataset.viewport = demoViewport;
  3148. }
  3149. const overlayRoot = root?.getElementById?.("overlayRoot");
  3150. const hasConsentOverlay = Boolean(overlayRoot) && hasConsentDialog(state);
  3151. const activeCommand = state?.overlays?.commandId ? state?.commandProjections?.[state.overlays.commandId] : null;
  3152. const hasCommandOverlay = Boolean(overlayRoot) && hasCommandDialog(state);
  3153. const hasReportServiceOverlay = Boolean(overlayRoot) && hasReportServiceDialog(state);
  3154. const nextModalIdentity = hasConsentOverlay ? `consent:${state.overlays.consentId}` : hasCommandOverlay ? `command:${activeCommand.commandId}:${activeCommand.status}` : hasReportServiceOverlay ? `report-service:${state.overlays.reportId}` : null;
  3155. const hasModalOverlay = Boolean(nextModalIdentity);
  3156. for (const element of [
  3157. elements.appHeader,
  3158. elements.appView,
  3159. elements.bottomNav,
  3160. scenarioPanel
  3161. ]) {
  3162. if (!element) {
  3163. continue;
  3164. }
  3165. element.inert = hasModalOverlay;
  3166. element.setAttribute?.(
  3167. "aria-hidden",
  3168. hasModalOverlay ? "true" : "false"
  3169. );
  3170. }
  3171. if (overlayRoot) {
  3172. overlayRoot.innerHTML = hasConsentOverlay ? renderConsentOverlay(state) : hasCommandOverlay ? renderCommandOverlay(activeCommand) : hasReportServiceOverlay ? renderReportServiceOverlay(state) : "";
  3173. if (nextModalIdentity && nextModalIdentity !== modalOverlayIdentity) {
  3174. focusModalDialog(overlayRoot);
  3175. } else if (!nextModalIdentity && modalOverlayIdentity) {
  3176. restoreConsentFocus(root, elements);
  3177. }
  3178. }
  3179. modalOverlayIdentity = nextModalIdentity;
  3180. renderIcons();
  3181. return true;
  3182. };
  3183. var demoState = loadScenario(createInitialState(), "EMPTY_HOME");
  3184. var boundDocuments = /* @__PURE__ */ new WeakSet();
  3185. var reportProjectionGeneration = 0;
  3186. var activeReportProjectionTimer = null;
  3187. var registrationSelectionFixtureSequence = 0;
  3188. var commandConfirmationInFlight = null;
  3189. var lastCommandConfirmationRequest = null;
  3190. var commandRejectionInFlight = null;
  3191. var lastCommandRejectionRequest = null;
  3192. var getDemoState = () => demoState;
  3193. var getLastCommandConfirmationRequest = () => lastCommandConfirmationRequest;
  3194. var getLastCommandRejectionRequest = () => lastCommandRejectionRequest;
  3195. var simulateCandidateSelectionBackendFixture = (state, interaction) => {
  3196. registrationSelectionFixtureSequence += 1;
  3197. const task = state?.activeAgentTaskProjection;
  3198. const result = task?.activeResultRef ? state?.resultSnapshotProjections?.[task.activeResultRef] : null;
  3199. const now = Date.parse(state?.demoNow);
  3200. const expiresAt = Date.parse(result?.expiresAt);
  3201. const versionMatched = interaction?.resultRef === result?.resultRef && interaction?.resultVersion === result?.resultVersion;
  3202. const notExpired = Number.isFinite(now) && Number.isFinite(expiresAt) && expiresAt > now;
  3203. const realtimeAvailable = Array.isArray(result?.items) && result.items.some(
  3204. (item) => item?.candidateId === interaction?.candidateId
  3205. );
  3206. const errorCode = !notExpired ? "RESULT_EXPIRED" : !versionMatched ? "RESULT_VERSION_CONFLICT" : !realtimeAvailable ? "SLOT_UNAVAILABLE" : null;
  3207. const fixtureId = `selection-response-${registrationSelectionFixtureSequence}`;
  3208. if (errorCode) {
  3209. return {
  3210. id: fixtureId,
  3211. errorCode,
  3212. validation: {
  3213. tenantMatched: true,
  3214. patientMatched: true,
  3215. sessionMatched: true,
  3216. versionMatched,
  3217. notExpired,
  3218. realtimeAvailable,
  3219. l1LockSucceeded: false
  3220. }
  3221. };
  3222. }
  3223. const lockExpiresAt = new Date(
  3224. Math.min(now + 5 * 60 * 1e3, expiresAt - 1)
  3225. ).toISOString();
  3226. return {
  3227. id: fixtureId,
  3228. outcome: "SUCCESS",
  3229. ...interaction,
  3230. validation: {
  3231. tenantMatched: true,
  3232. patientMatched: true,
  3233. sessionMatched: true,
  3234. versionMatched,
  3235. notExpired,
  3236. realtimeAvailable,
  3237. l1LockSucceeded: true
  3238. },
  3239. lock: {
  3240. lockId: `appointment-lock-${registrationSelectionFixtureSequence}`,
  3241. expiresAt: lockExpiresAt
  3242. }
  3243. };
  3244. };
  3245. var submitAssistantText = (state, text) => {
  3246. const normalizedText = typeof text === "string" ? text.trim() : text;
  3247. const interaction = buildCandidateSelectionInteraction(
  3248. state,
  3249. normalizedText
  3250. );
  3251. if (!interaction && !(normalizedText === "\u9009\u7B2C\u4E00\u4E2A" && state?.activeAgentTaskProjection?.type === "REGISTRATION")) {
  3252. return submitInteraction(state, normalizedText);
  3253. }
  3254. const responseFixture = simulateCandidateSelectionBackendFixture(
  3255. state,
  3256. interaction
  3257. );
  3258. const selectedState = applyCandidateSelectionResponseFixture(
  3259. state,
  3260. responseFixture
  3261. );
  3262. return applyRegistrationCommandBackendFixture(selectedState);
  3263. };
  3264. var applyRegistrationCommandBackendFixture = (state) => {
  3265. const task = state?.activeAgentTaskProjection;
  3266. const result = state?.resultSnapshotProjections?.[task?.activeResultRef];
  3267. const selected = result?.items?.find(
  3268. (item) => item?.candidateId === task?.selectedCandidateId
  3269. );
  3270. if (task?.stage !== "AWAITING_REGISTRATION_CONFIRMATION" || !selected) {
  3271. return state;
  3272. }
  3273. const commands = Object.values(state?.commandProjections ?? {});
  3274. const nonTerminalStatuses = /* @__PURE__ */ new Set([
  3275. "PREPARED",
  3276. "AWAITING_CONFIRMATION",
  3277. "EXECUTING",
  3278. "UNKNOWN"
  3279. ]);
  3280. const existing = commands.find((command) => command?.details?.candidateId === selected.candidateId && nonTerminalStatuses.has(command.status));
  3281. if (existing) {
  3282. return {
  3283. ...state,
  3284. overlays: { ...state.overlays, commandId: existing.commandId }
  3285. };
  3286. }
  3287. const nextSequence = commands.reduce((highest, command) => {
  3288. const sequence = Number.parseInt(
  3289. command?.commandId?.match(/^command-register-(\d+)$/)?.[1],
  3290. 10
  3291. );
  3292. return Number.isInteger(sequence) ? Math.max(highest, sequence) : highest;
  3293. }, 0) + 1;
  3294. const commandId = `command-register-${String(nextSequence).padStart(3, "0")}`;
  3295. return applyCommandProjectionFixture(state, {
  3296. id: `command-awaiting-${commandId}`,
  3297. commandId,
  3298. version: 1,
  3299. riskLevel: "L2",
  3300. status: "AWAITING_CONFIRMATION",
  3301. details: {
  3302. operationName: "\u786E\u8BA4\u6302\u53F7",
  3303. patientName: "\u5F20\u5148\u751F",
  3304. departmentName: selected.departmentName,
  3305. doctorName: selected.doctorName,
  3306. doctorTitle: selected.doctorTitle,
  3307. visitAt: `${selected.visitDate} ${selected.visitTime}`,
  3308. fee: selected.fee,
  3309. expiresAt: task.lock?.expiresAt,
  3310. countdown: "\u5269\u4F59 5 \u5206\u949F",
  3311. candidateId: selected.candidateId
  3312. }
  3313. });
  3314. };
  3315. var reduceDemoAction = (state, action, reportId, candidateId, assistantText, appointmentId, tabId, filter, messageId, scenarioName, viewport) => {
  3316. switch (action) {
  3317. case "view-report":
  3318. return openReportDetails(state, reportId);
  3319. case "view-original-report":
  3320. return state.reportSourceProjections?.[reportId] ? {
  3321. ...state,
  3322. overlays: {
  3323. ...state.overlays,
  3324. reportId,
  3325. reportView: "ORIGINAL_REPORT"
  3326. }
  3327. } : state;
  3328. case "open-report-center": {
  3329. const reportCenterState = closeReportView(state);
  3330. return {
  3331. ...reportCenterState,
  3332. activeTab: "records",
  3333. reportFilters: {
  3334. ...reportCenterState.reportFilters,
  3335. family: ["LAB", "EXAM"].includes(filter) ? filter : "ALL"
  3336. },
  3337. overlays: {
  3338. ...reportCenterState.overlays,
  3339. reportView: "REPORT_CENTER"
  3340. }
  3341. };
  3342. }
  3343. case "set-report-family-filter":
  3344. return {
  3345. ...state,
  3346. activeTab: "records",
  3347. reportFilters: {
  3348. ...state.reportFilters,
  3349. family: ["ALL", "LAB", "EXAM"].includes(filter) ? filter : "ALL"
  3350. }
  3351. };
  3352. case "set-report-status-filter":
  3353. return {
  3354. ...state,
  3355. activeTab: "records",
  3356. reportFilters: {
  3357. ...state.reportFilters,
  3358. status: ["ALL", "UNREAD", "READ"].includes(filter) ? filter : "ALL"
  3359. }
  3360. };
  3361. case "toggle-report-metrics":
  3362. return {
  3363. ...state,
  3364. overlays: {
  3365. ...state.overlays,
  3366. reportExpanded: !state.overlays?.reportExpanded
  3367. }
  3368. };
  3369. case "interpret-report":
  3370. return requestReportInterpretation(state, reportId);
  3371. case "grant-report-consent":
  3372. return grantConsent(state, state?.overlays?.consentId);
  3373. case "reject-report-consent":
  3374. return rejectConsent(state, state?.overlays?.consentId);
  3375. case "open-report-feedback":
  3376. return reportId === state?.overlays?.reportId && state?.overlays?.reportView === "INTERPRETED" && hasValidReportConsent(state, reportId) && selectValidReportResult(state, reportId) ? {
  3377. ...state,
  3378. overlays: {
  3379. ...state.overlays,
  3380. reportFeedbackOpen: true,
  3381. reportFeedbackSubmitted: false,
  3382. reportServiceNotice: null
  3383. }
  3384. } : state;
  3385. case "submit-report-feedback": {
  3386. if (reportId !== state?.overlays?.reportId || state?.overlays?.reportView !== "INTERPRETED") {
  3387. return state;
  3388. }
  3389. const feedbackState = submitReportFeedback(state, {
  3390. reportId,
  3391. category: filter
  3392. });
  3393. return feedbackState === state ? state : {
  3394. ...feedbackState,
  3395. overlays: {
  3396. ...feedbackState.overlays,
  3397. reportFeedbackOpen: false,
  3398. reportFeedbackSubmitted: true,
  3399. reportServiceNotice: null
  3400. }
  3401. };
  3402. }
  3403. case "consult-ordering-doctor":
  3404. case "online-consultation":
  3405. case "contact-human-service": {
  3406. if (action === "contact-human-service" && reportId === state?.overlays?.reportId && state?.overlays?.reportView === "REPORT_UNAVAILABLE" && state?.reportSourceProjections?.[reportId]) {
  3407. return {
  3408. ...state,
  3409. overlays: {
  3410. ...state.overlays,
  3411. reportServiceNotice: "\u5DF2\u4E3A\u4F60\u6253\u5F00\u4EBA\u5DE5\u670D\u52A1\u5165\u53E3\u3002"
  3412. }
  3413. };
  3414. }
  3415. if (reportId !== state?.overlays?.reportId || state?.overlays?.reportView !== "INTERPRETED" || !hasValidReportConsent(state, reportId) || !selectValidReportResult(state, reportId)) {
  3416. return state;
  3417. }
  3418. const notices = {
  3419. "consult-ordering-doctor": "\u5DF2\u4E3A\u4F60\u6253\u5F00\u5F00\u5355\u533B\u751F\u54A8\u8BE2\u5165\u53E3\u3002",
  3420. "online-consultation": "\u5DF2\u4E3A\u4F60\u6253\u5F00\u5728\u7EBF\u54A8\u8BE2\u5165\u53E3\u3002",
  3421. "contact-human-service": "\u5DF2\u4E3A\u4F60\u6253\u5F00\u4EBA\u5DE5\u670D\u52A1\u5165\u53E3\u3002"
  3422. };
  3423. return {
  3424. ...state,
  3425. overlays: {
  3426. ...state.overlays,
  3427. reportFeedbackOpen: false,
  3428. reportServiceNotice: notices[action]
  3429. }
  3430. };
  3431. }
  3432. case "close-report-service":
  3433. return hasReportServiceDialog(state) ? {
  3434. ...state,
  3435. overlays: {
  3436. ...state.overlays,
  3437. reportServiceNotice: null
  3438. }
  3439. } : state;
  3440. case "revoke-report-consent": {
  3441. const activeReportId = state?.overlays?.reportId;
  3442. const activeConsentId = typeof activeReportId === "string" ? `consent-ai-${activeReportId}` : Object.keys(state?.consents ?? {}).find(
  3443. (consentId) => state.consents[consentId]?.status === "GRANTED"
  3444. );
  3445. return revokeConsent(state, activeConsentId);
  3446. }
  3447. case "go-home": {
  3448. const homeState = closeReportView(state);
  3449. return {
  3450. ...homeState,
  3451. activeTab: "home",
  3452. overlays: {
  3453. ...homeState.overlays,
  3454. appointmentId: null,
  3455. journeyView: null
  3456. }
  3457. };
  3458. }
  3459. case "open-checkin-confirmation":
  3460. return {
  3461. ...state,
  3462. overlays: {
  3463. ...state.overlays,
  3464. appointmentId,
  3465. journeyView: "CHECKIN_CONFIRMATION"
  3466. }
  3467. };
  3468. case "open-queue":
  3469. return {
  3470. ...state,
  3471. overlays: {
  3472. ...state.overlays,
  3473. appointmentId,
  3474. journeyView: "QUEUE"
  3475. }
  3476. };
  3477. case "open-route":
  3478. return {
  3479. ...state,
  3480. overlays: {
  3481. ...state.overlays,
  3482. appointmentId,
  3483. journeyView: "ROUTE"
  3484. }
  3485. };
  3486. case "switch-patient": {
  3487. commandConfirmationInFlight = null;
  3488. commandRejectionInFlight = null;
  3489. invalidateReportProjectionTimer();
  3490. const nextPatientId = state.activePatientId === "patient-li-na" ? "patient-zhang-san" : "patient-li-na";
  3491. return {
  3492. ...createInitialState({ activePatientId: nextPatientId }),
  3493. demoScenario: "EMPTY_HOME"
  3494. };
  3495. }
  3496. case "submit-checkin": {
  3497. const checkedIn = applyServerProjectionFixture(state, {
  3498. id: `checkin-completed-${appointmentId}`,
  3499. type: "CHECKIN_COMPLETED",
  3500. occurredAt: state.demoNow,
  3501. payload: {
  3502. appointmentId,
  3503. queueNumber: "A023",
  3504. room: "3F-08\u8BCA\u5BA4"
  3505. }
  3506. });
  3507. return {
  3508. ...checkedIn,
  3509. overlays: {
  3510. ...checkedIn.overlays,
  3511. appointmentId,
  3512. journeyView: "QUEUE"
  3513. }
  3514. };
  3515. }
  3516. case "open-assistant":
  3517. case "return-current-task":
  3518. return {
  3519. ...closeReportView(state),
  3520. activeTab: "assistant"
  3521. };
  3522. case "navigate-tab": {
  3523. const tabState = closeReportView(state);
  3524. return {
  3525. ...tabState,
  3526. activeTab: navItems.some((item) => item.id === tabId) ? tabId : "home",
  3527. systemState: null,
  3528. overlays: {
  3529. ...tabState.overlays,
  3530. appointmentId: null,
  3531. journeyView: null
  3532. }
  3533. };
  3534. }
  3535. case "open-messages":
  3536. return {
  3537. ...closeReportView(state),
  3538. activeTab: "messages",
  3539. systemState: null
  3540. };
  3541. case "set-record-filter":
  3542. return {
  3543. ...state,
  3544. activeTab: "records",
  3545. recordsFilter: ["all", "abnormal", "unread"].includes(filter) ? filter : "all"
  3546. };
  3547. case "open-message": {
  3548. const message = (Array.isArray(state.messages) ? state.messages : []).find((item) => item?.id === messageId);
  3549. if (message?.type === "REPORT") {
  3550. const reportId2 = typeof message.reportId === "string" ? message.reportId : null;
  3551. return reportId2 ? openReportDetails(state, reportId2) : state;
  3552. }
  3553. if (message?.type === "JOURNEY") {
  3554. const journey = state.journeyProjections?.[0];
  3555. return journey ? {
  3556. ...state,
  3557. activeTab: journey.stage === "QUEUE_WAITING" ? state.activeTab : "care",
  3558. overlays: {
  3559. ...state.overlays,
  3560. appointmentId: journey.appointmentId,
  3561. journeyView: journey.stage === "QUEUE_WAITING" ? "QUEUE" : null
  3562. }
  3563. } : state;
  3564. }
  3565. return state;
  3566. }
  3567. case "load-scenario":
  3568. case "reset-scenario": {
  3569. commandConfirmationInFlight = null;
  3570. commandRejectionInFlight = null;
  3571. invalidateReportProjectionTimer();
  3572. return loadScenario(
  3573. state,
  3574. action === "reset-scenario" ? state.demoScenario ?? "EMPTY_HOME" : scenarioName
  3575. );
  3576. }
  3577. case "previous-scenario":
  3578. case "next-scenario": {
  3579. const currentIndex = Math.max(
  3580. 0,
  3581. demoScenarios.indexOf(state.demoScenario)
  3582. );
  3583. const delta = action === "previous-scenario" ? -1 : 1;
  3584. const nextIndex = (currentIndex + delta + demoScenarios.length) % demoScenarios.length;
  3585. commandConfirmationInFlight = null;
  3586. commandRejectionInFlight = null;
  3587. invalidateReportProjectionTimer();
  3588. return loadScenario(state, demoScenarios[nextIndex]);
  3589. }
  3590. case "toggle-scenario-panel":
  3591. scenarioPanelVisible = !scenarioPanelVisible;
  3592. return state;
  3593. case "set-demo-viewport":
  3594. demoViewport = viewport === "430" ? "430" : "390";
  3595. return state;
  3596. case "submit-registration-demo":
  3597. return submitAssistantText(
  3598. state,
  3599. "\u6211\u60F3\u6302\u660E\u5929\u795E\u7ECF\u5185\u79D1\u674E\u660E\u4E3B\u4EFB\u7684\u53F7"
  3600. );
  3601. case "submit-assistant-text":
  3602. return submitAssistantText(state, assistantText);
  3603. case "select-candidate": {
  3604. const interaction = buildCandidateSelectionInteraction(
  3605. state,
  3606. candidateId
  3607. );
  3608. return applyRegistrationCommandBackendFixture(
  3609. applyCandidateSelectionResponseFixture(
  3610. state,
  3611. simulateCandidateSelectionBackendFixture(state, interaction)
  3612. )
  3613. );
  3614. }
  3615. case "close-command":
  3616. return {
  3617. ...state,
  3618. activeTab: "home",
  3619. overlays: { ...state.overlays, commandId: null }
  3620. };
  3621. case "requery-registration":
  3622. commandConfirmationInFlight = null;
  3623. return submitInteraction({
  3624. ...state,
  3625. overlays: { ...state.overlays, commandId: null }
  3626. }, "\u6211\u60F3\u6302\u660E\u5929\u795E\u7ECF\u5185\u79D1\u674E\u660E\u4E3B\u4EFB\u7684\u53F7");
  3627. default:
  3628. return state;
  3629. }
  3630. };
  3631. var selectInterpretingReportId = (state) => {
  3632. const reportId = state?.overlays?.reportId;
  3633. const hasInterpretingItem = state?.patientWorkItems?.some(
  3634. (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "INTERPRETING"
  3635. );
  3636. return typeof reportId === "string" && state?.overlays?.reportView === "INTERPRETING" && evaluateReportInterpretationGate(state, reportId).allowed && state?.reportSourceProjections?.[reportId]?.interpretationStatus === "GENERATING" && hasValidReportConsent(state, reportId) && hasInterpretingItem ? reportId : null;
  3637. };
  3638. var invalidateReportProjectionTimer = () => {
  3639. activeReportProjectionTimer = null;
  3640. reportProjectionGeneration += 1;
  3641. };
  3642. var ensureReportInterpretationProjection = () => {
  3643. if (demoState?.demoScenario === "REPORT_GENERATING") {
  3644. invalidateReportProjectionTimer();
  3645. return;
  3646. }
  3647. const reportId = selectInterpretingReportId(demoState);
  3648. if (!reportId) {
  3649. invalidateReportProjectionTimer();
  3650. return;
  3651. }
  3652. if (activeReportProjectionTimer?.reportId === reportId && activeReportProjectionTimer.generation === reportProjectionGeneration) {
  3653. return;
  3654. }
  3655. if (typeof globalThis.setTimeout !== "function") {
  3656. return;
  3657. }
  3658. const scheduledGeneration = reportProjectionGeneration;
  3659. const consentId = `consent-ai-${reportId}`;
  3660. const timerToken = {
  3661. generation: scheduledGeneration,
  3662. reportId,
  3663. patientId: demoState?.activePatientId,
  3664. stateContext: demoState,
  3665. consentContext: demoState?.consents?.[consentId],
  3666. bindingContext: demoState?.consentScopeBindings
  3667. };
  3668. activeReportProjectionTimer = timerToken;
  3669. globalThis.setTimeout(() => {
  3670. if (activeReportProjectionTimer !== timerToken) {
  3671. return;
  3672. }
  3673. activeReportProjectionTimer = null;
  3674. const isCurrentInterpretation = reportProjectionGeneration === scheduledGeneration && selectInterpretingReportId(demoState) === reportId && demoState?.activePatientId === timerToken.patientId && demoState === timerToken.stateContext && demoState?.consents?.[consentId] === timerToken.consentContext && demoState?.consentScopeBindings === timerToken.bindingContext;
  3675. if (!isCurrentInterpretation) {
  3676. return;
  3677. }
  3678. demoState = applyReportInterpretationProjectionFixture(
  3679. demoState,
  3680. reportId
  3681. );
  3682. renderAppShell(demoState);
  3683. }, 120);
  3684. };
  3685. var beginCommandConfirmation = () => {
  3686. const commandId = demoState?.overlays?.commandId;
  3687. if (commandConfirmationInFlight || commandRejectionInFlight || !commandId) {
  3688. return;
  3689. }
  3690. lastCommandConfirmationRequest = buildCommandConfirmationRequest(
  3691. demoState,
  3692. commandId,
  3693. {
  3694. channel: "CARD",
  3695. idempotencyKey: `confirm-${commandId.replace(/^command-/, "")}`
  3696. }
  3697. );
  3698. const token = { commandId, request: lastCommandConfirmationRequest };
  3699. commandConfirmationInFlight = token;
  3700. if (typeof globalThis.setTimeout !== "function") {
  3701. return;
  3702. }
  3703. globalThis.setTimeout(() => {
  3704. if (commandConfirmationInFlight !== token || demoState?.commandProjections?.[commandId]?.status !== "AWAITING_CONFIRMATION") {
  3705. return;
  3706. }
  3707. demoState = applyCommandProjectionFixture(demoState, {
  3708. id: `command-executing-${commandId}`,
  3709. commandId,
  3710. version: demoState.commandProjections[commandId].version + 1,
  3711. status: "EXECUTING"
  3712. });
  3713. renderAppShell(demoState);
  3714. globalThis.setTimeout(() => {
  3715. if (commandConfirmationInFlight !== token || demoState?.commandProjections?.[commandId]?.status !== "EXECUTING") {
  3716. return;
  3717. }
  3718. demoState = applyCommandProjectionFixture(demoState, {
  3719. id: `command-succeeded-${commandId}`,
  3720. commandId,
  3721. version: demoState.commandProjections[commandId].version + 1,
  3722. status: "SUCCEEDED",
  3723. result: {
  3724. type: "APPOINTMENT_CREATED",
  3725. eventId: `appointment-event-${commandId}`,
  3726. appointmentId: `appointment-${commandId}`,
  3727. patientId: demoState.activePatientId,
  3728. summary: {
  3729. visitDate: demoState.commandProjections[commandId].details?.visitAt?.split(" ")?.[0],
  3730. visitTime: demoState.commandProjections[commandId].details?.visitAt?.split(" ")?.[1],
  3731. departmentName: demoState.commandProjections[commandId].details?.departmentName,
  3732. doctorName: demoState.commandProjections[commandId].details?.doctorName,
  3733. doctorTitle: demoState.commandProjections[commandId].details?.doctorTitle,
  3734. fee: demoState.commandProjections[commandId].details?.fee
  3735. }
  3736. }
  3737. });
  3738. commandConfirmationInFlight = null;
  3739. renderAppShell(demoState);
  3740. }, 120);
  3741. }, 80);
  3742. };
  3743. var beginCommandRejection = () => {
  3744. const commandId = demoState?.overlays?.commandId;
  3745. if (commandRejectionInFlight || commandConfirmationInFlight || !commandId) {
  3746. return;
  3747. }
  3748. lastCommandRejectionRequest = buildCommandRejectionRequest(
  3749. demoState,
  3750. commandId,
  3751. {
  3752. reason: "\u6682\u4E0D\u64CD\u4F5C",
  3753. idempotencyKey: `reject-${commandId.replace(/^command-/, "")}`
  3754. }
  3755. );
  3756. const token = { commandId, request: lastCommandRejectionRequest };
  3757. commandRejectionInFlight = token;
  3758. if (typeof globalThis.setTimeout !== "function") {
  3759. return;
  3760. }
  3761. globalThis.setTimeout(() => {
  3762. if (commandRejectionInFlight !== token || demoState?.commandProjections?.[commandId]?.status !== "AWAITING_CONFIRMATION") {
  3763. return;
  3764. }
  3765. demoState = applyCommandProjectionFixture(demoState, {
  3766. id: `command-rejected-${commandId}`,
  3767. commandId,
  3768. version: demoState.commandProjections[commandId].version + 1,
  3769. status: "REJECTED"
  3770. });
  3771. commandRejectionInFlight = null;
  3772. renderAppShell(demoState);
  3773. }, 80);
  3774. };
  3775. var initializePatientServiceDesk = (state = demoState) => {
  3776. if (state !== demoState) {
  3777. invalidateReportProjectionTimer();
  3778. commandConfirmationInFlight = null;
  3779. commandRejectionInFlight = null;
  3780. lastCommandConfirmationRequest = null;
  3781. lastCommandRejectionRequest = null;
  3782. }
  3783. demoState = state;
  3784. const root = globalThis.document;
  3785. if (root?.addEventListener && !boundDocuments.has(root)) {
  3786. root.addEventListener("click", (event) => {
  3787. const trigger = event.target?.closest?.("[data-action]");
  3788. const overlayRoot = root.getElementById?.("overlayRoot");
  3789. const appView = root.getElementById?.("appView");
  3790. const scenarioPanel = root.getElementById?.("scenarioPanel");
  3791. const appShell = root.querySelector?.(".phone-shell") ?? appView?.closest?.(".phone-shell");
  3792. const withinApp = Boolean(
  3793. appShell?.contains?.(trigger) || appView?.contains?.(trigger) || overlayRoot?.contains?.(trigger) || scenarioPanel?.contains?.(trigger)
  3794. );
  3795. if (!trigger || !withinApp) {
  3796. return;
  3797. }
  3798. if ((hasConsentDialog(demoState) || hasCommandDialog(demoState) || hasReportServiceDialog(demoState)) && scenarioPanel?.contains?.(trigger)) {
  3799. return;
  3800. }
  3801. if (trigger.dataset?.action === "interpret-report") {
  3802. consentFocusReturn = {
  3803. reportId: trigger.dataset?.reportId
  3804. };
  3805. }
  3806. if (trigger.dataset?.action === "confirm-command") {
  3807. beginCommandConfirmation();
  3808. return;
  3809. }
  3810. if (trigger.dataset?.action === "reject-command") {
  3811. beginCommandRejection();
  3812. return;
  3813. }
  3814. demoState = reduceDemoAction(
  3815. demoState,
  3816. trigger.dataset?.action,
  3817. trigger.dataset?.reportId,
  3818. trigger.dataset?.candidateId,
  3819. root.getElementById?.("assistantComposerInput")?.value,
  3820. trigger.dataset?.appointmentId,
  3821. trigger.dataset?.tabId,
  3822. trigger.dataset?.filter,
  3823. trigger.dataset?.messageId,
  3824. trigger.dataset?.scenario,
  3825. trigger.dataset?.viewport
  3826. );
  3827. renderAppShell(demoState);
  3828. ensureReportInterpretationProjection();
  3829. });
  3830. root.addEventListener("keydown", (event) => {
  3831. if (event.key === "Enter" && event.target?.id === "assistantComposerInput") {
  3832. const appView = root.getElementById?.("appView");
  3833. if (!appView?.contains?.(event.target)) {
  3834. return;
  3835. }
  3836. event.preventDefault?.();
  3837. demoState = reduceDemoAction(
  3838. demoState,
  3839. "submit-assistant-text",
  3840. void 0,
  3841. void 0,
  3842. event.target.value
  3843. );
  3844. renderAppShell(demoState);
  3845. ensureReportInterpretationProjection();
  3846. return;
  3847. }
  3848. const targetTag = event.target?.tagName?.toLowerCase?.();
  3849. const isTyping = ["input", "textarea", "select"].includes(targetTag) || event.target?.isContentEditable;
  3850. const isPresentationShortcut = !isTyping && ["ArrowLeft", "ArrowRight", "r", "R", "p", "P"].includes(event.key);
  3851. const consentDialogOpen = hasConsentDialog(demoState);
  3852. const commandDialogOpen = hasCommandDialog(demoState);
  3853. const reportServiceDialogOpen = hasReportServiceDialog(demoState);
  3854. if (isPresentationShortcut && (consentDialogOpen || commandDialogOpen || reportServiceDialogOpen)) {
  3855. event.preventDefault?.();
  3856. return;
  3857. }
  3858. if (isPresentationShortcut) {
  3859. event.preventDefault?.();
  3860. if (event.key === "p" || event.key === "P") {
  3861. scenarioPanelVisible = !scenarioPanelVisible;
  3862. renderAppShell(demoState);
  3863. return;
  3864. }
  3865. const currentIndex = Math.max(
  3866. 0,
  3867. demoScenarios.indexOf(demoState.demoScenario)
  3868. );
  3869. const nextScenario = event.key === "r" || event.key === "R" ? demoState.demoScenario ?? "EMPTY_HOME" : demoScenarios[(currentIndex + (event.key === "ArrowLeft" ? -1 : 1) + demoScenarios.length) % demoScenarios.length];
  3870. initializePatientServiceDesk(loadScenario(demoState, nextScenario));
  3871. return;
  3872. }
  3873. if (!consentDialogOpen && !commandDialogOpen && !reportServiceDialogOpen) {
  3874. return;
  3875. }
  3876. if (event.key === "Escape" && reportServiceDialogOpen) {
  3877. event.preventDefault?.();
  3878. demoState = reduceDemoAction(
  3879. demoState,
  3880. "close-report-service"
  3881. );
  3882. renderAppShell(demoState);
  3883. return;
  3884. }
  3885. if (event.key === "Escape" && consentDialogOpen) {
  3886. event.preventDefault?.();
  3887. demoState = rejectConsent(
  3888. demoState,
  3889. demoState.overlays.consentId
  3890. );
  3891. renderAppShell(demoState);
  3892. ensureReportInterpretationProjection();
  3893. return;
  3894. }
  3895. if (event.key !== "Tab") {
  3896. return;
  3897. }
  3898. const overlayRoot = root.getElementById?.("overlayRoot");
  3899. const focusables = Array.from(
  3900. overlayRoot?.querySelectorAll?.(
  3901. 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  3902. ) ?? []
  3903. );
  3904. if (focusables.length === 0) {
  3905. event.preventDefault?.();
  3906. overlayRoot?.querySelector?.('[role="dialog"]')?.focus?.();
  3907. return;
  3908. }
  3909. const activeIndex = focusables.indexOf(root.activeElement);
  3910. const nextIndex = event.shiftKey ? activeIndex <= 0 ? focusables.length - 1 : activeIndex - 1 : activeIndex === focusables.length - 1 ? 0 : activeIndex + 1;
  3911. event.preventDefault?.();
  3912. focusables[nextIndex]?.focus?.();
  3913. });
  3914. boundDocuments.add(root);
  3915. }
  3916. const rendered = renderAppShell(demoState);
  3917. ensureReportInterpretationProjection();
  3918. return rendered;
  3919. };
  3920. if (globalThis.document) {
  3921. if (globalThis.document.readyState === "loading") {
  3922. globalThis.document.addEventListener(
  3923. "DOMContentLoaded",
  3924. () => initializePatientServiceDesk(),
  3925. { once: true }
  3926. );
  3927. } else {
  3928. initializePatientServiceDesk();
  3929. }
  3930. }
  3931. })();