vintasend 0.4.0 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/README.md +5 -0
  2. package/dist/examples/vintasend-medplum-example/.env.example +11 -0
  3. package/dist/examples/vintasend-medplum-example/IMPLEMENTATION_PLAN_FILE_ATTACHMENTS.md +597 -0
  4. package/dist/examples/vintasend-medplum-example/README.md +190 -0
  5. package/dist/examples/vintasend-medplum-example/TUTORIAL_EMAIL_NOTIFICATIONS.md +2596 -0
  6. package/dist/examples/vintasend-medplum-example/bots/handlers/send-pending-notifications-bot.ts +39 -0
  7. package/dist/examples/vintasend-medplum-example/bots/handlers/send-task-assignment-email.ts +41 -0
  8. package/dist/examples/vintasend-medplum-example/bots/handlers/task-due-soon-notification-bot.ts +86 -0
  9. package/dist/examples/vintasend-medplum-example/bots/index.ts +53 -0
  10. package/dist/examples/vintasend-medplum-example/bots/services/emails/schedule-task-due-soon-email.ts +84 -0
  11. package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.test.ts +388 -0
  12. package/dist/examples/vintasend-medplum-example/bots/services/emails/send-task-assignment-email.ts +113 -0
  13. package/dist/examples/vintasend-medplum-example/bots/shared/task-due-soon-helpers.ts +115 -0
  14. package/dist/examples/vintasend-medplum-example/bots/task-assignment-bot.ts +41 -0
  15. package/dist/examples/vintasend-medplum-example/compiled-notification-templates.json +6 -0
  16. package/dist/examples/vintasend-medplum-example/esbuild-script.mjs +71 -0
  17. package/dist/examples/vintasend-medplum-example/index.html +14 -0
  18. package/dist/examples/vintasend-medplum-example/lib/constants.ts +32 -0
  19. package/dist/examples/vintasend-medplum-example/lib/extensions.ts +1 -0
  20. package/dist/examples/vintasend-medplum-example/lib/file-upload.test.ts +389 -0
  21. package/dist/examples/vintasend-medplum-example/lib/file-upload.ts +222 -0
  22. package/dist/examples/vintasend-medplum-example/lib/medplum-singleton.ts +18 -0
  23. package/dist/examples/vintasend-medplum-example/lib/notification-service.test.ts +293 -0
  24. package/dist/examples/vintasend-medplum-example/lib/notification-service.ts +284 -0
  25. package/dist/examples/vintasend-medplum-example/lib/patients.ts +20 -0
  26. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/body.html.pug +37 -0
  27. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-assignment/subject.txt.pug +4 -0
  28. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/body.html.pug +34 -0
  29. package/dist/examples/vintasend-medplum-example/notification-templates/emails/task-due-soon/subject.txt.pug +4 -0
  30. package/dist/examples/vintasend-medplum-example/package.json +75 -0
  31. package/dist/examples/vintasend-medplum-example/plugins/gql-plugin.mjs +31 -0
  32. package/dist/examples/vintasend-medplum-example/postcss.config.mjs +21 -0
  33. package/dist/examples/vintasend-medplum-example/public/favicon.ico +0 -0
  34. package/dist/examples/vintasend-medplum-example/public/img/integrations/acuity.png +0 -0
  35. package/dist/examples/vintasend-medplum-example/public/img/integrations/auth0.png +0 -0
  36. package/dist/examples/vintasend-medplum-example/public/img/integrations/azure.png +0 -0
  37. package/dist/examples/vintasend-medplum-example/public/img/integrations/calcom.png +0 -0
  38. package/dist/examples/vintasend-medplum-example/public/img/integrations/candid.png +0 -0
  39. package/dist/examples/vintasend-medplum-example/public/img/integrations/claude.png +0 -0
  40. package/dist/examples/vintasend-medplum-example/public/img/integrations/datadog.png +0 -0
  41. package/dist/examples/vintasend-medplum-example/public/img/integrations/deepseek.png +0 -0
  42. package/dist/examples/vintasend-medplum-example/public/img/integrations/entra.png +0 -0
  43. package/dist/examples/vintasend-medplum-example/public/img/integrations/epic.png +0 -0
  44. package/dist/examples/vintasend-medplum-example/public/img/integrations/google.png +0 -0
  45. package/dist/examples/vintasend-medplum-example/public/img/integrations/healthgorilla.png +0 -0
  46. package/dist/examples/vintasend-medplum-example/public/img/integrations/healthie.png +0 -0
  47. package/dist/examples/vintasend-medplum-example/public/img/integrations/labcorp.png +0 -0
  48. package/dist/examples/vintasend-medplum-example/public/img/integrations/okta.png +0 -0
  49. package/dist/examples/vintasend-medplum-example/public/img/integrations/openai.png +0 -0
  50. package/dist/examples/vintasend-medplum-example/public/img/integrations/particle.png +0 -0
  51. package/dist/examples/vintasend-medplum-example/public/img/integrations/quest.png +0 -0
  52. package/dist/examples/vintasend-medplum-example/public/img/integrations/recaptcha.png +0 -0
  53. package/dist/examples/vintasend-medplum-example/public/img/integrations/snowflake.png +0 -0
  54. package/dist/examples/vintasend-medplum-example/public/img/integrations/stedi.png +0 -0
  55. package/dist/examples/vintasend-medplum-example/public/img/integrations/stripe.png +0 -0
  56. package/dist/examples/vintasend-medplum-example/public/img/integrations/sumo.png +0 -0
  57. package/dist/examples/vintasend-medplum-example/scripts/README.md +162 -0
  58. package/dist/examples/vintasend-medplum-example/scripts/client.ts +18 -0
  59. package/dist/examples/vintasend-medplum-example/scripts/deploy-bots.ts +171 -0
  60. package/dist/examples/vintasend-medplum-example/src/App.tsx +185 -0
  61. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.test.tsx +350 -0
  62. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemList.tsx +241 -0
  63. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.test.tsx +616 -0
  64. package/dist/examples/vintasend-medplum-example/src/components/ChargeItem/ChargeItemPanel.tsx +138 -0
  65. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.test.tsx +92 -0
  66. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionItem.tsx +47 -0
  67. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.test.tsx +464 -0
  68. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionList.tsx +186 -0
  69. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.test.tsx +80 -0
  70. package/dist/examples/vintasend-medplum-example/src/components/Conditions/ConditionModal.tsx +82 -0
  71. package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.test.tsx +100 -0
  72. package/dist/examples/vintasend-medplum-example/src/components/DoseSpotIcon.tsx +20 -0
  73. package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.module.css +3 -0
  74. package/dist/examples/vintasend-medplum-example/src/components/IntegrationCard.tsx +62 -0
  75. package/dist/examples/vintasend-medplum-example/src/components/MessageWithLinks.tsx +47 -0
  76. package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.test.tsx +299 -0
  77. package/dist/examples/vintasend-medplum-example/src/components/PerformingLabInput.tsx +52 -0
  78. package/dist/examples/vintasend-medplum-example/src/components/ResourceFormWithRequiredProfile.tsx +82 -0
  79. package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.test.tsx +1016 -0
  80. package/dist/examples/vintasend-medplum-example/src/components/encounter/BillingTab.tsx +298 -0
  81. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.test.tsx +732 -0
  82. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterChart.tsx +282 -0
  83. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.test.tsx +268 -0
  84. package/dist/examples/vintasend-medplum-example/src/components/encounter/EncounterHeader.tsx +224 -0
  85. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.test.tsx +255 -0
  86. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignAddendum.tsx +212 -0
  87. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.test.tsx +120 -0
  88. package/dist/examples/vintasend-medplum-example/src/components/encounter/SignLockDialog.tsx +57 -0
  89. package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.test.tsx +224 -0
  90. package/dist/examples/vintasend-medplum-example/src/components/encounter/VisitDetailsPanel.tsx +100 -0
  91. package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.test.tsx +431 -0
  92. package/dist/examples/vintasend-medplum-example/src/components/labs/CoverageInput.tsx +130 -0
  93. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.module.css +31 -0
  94. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.test.tsx +234 -0
  95. package/dist/examples/vintasend-medplum-example/src/components/labs/LabListItem.tsx +143 -0
  96. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.module.css +11 -0
  97. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.test.tsx +875 -0
  98. package/dist/examples/vintasend-medplum-example/src/components/labs/LabOrderDetails.tsx +943 -0
  99. package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.test.tsx +413 -0
  100. package/dist/examples/vintasend-medplum-example/src/components/labs/LabResultDetails.tsx +203 -0
  101. package/dist/examples/vintasend-medplum-example/src/components/labs/LabSelectEmpty.tsx +22 -0
  102. package/dist/examples/vintasend-medplum-example/src/components/labs/README.md +104 -0
  103. package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.test.tsx +318 -0
  104. package/dist/examples/vintasend-medplum-example/src/components/labs/TestMetadataCardInput.tsx +87 -0
  105. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.test.tsx +126 -0
  106. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatList.tsx +38 -0
  107. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.module.css +23 -0
  108. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.test.tsx +167 -0
  109. package/dist/examples/vintasend-medplum-example/src/components/messages/ChatListItem.tsx +53 -0
  110. package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.test.tsx +94 -0
  111. package/dist/examples/vintasend-medplum-example/src/components/messages/NewTopicDialog.tsx +165 -0
  112. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.module.css +8 -0
  113. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.test.tsx +523 -0
  114. package/dist/examples/vintasend-medplum-example/src/components/messages/ParticipantFilter.tsx +230 -0
  115. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.module.css +23 -0
  116. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.test.tsx +567 -0
  117. package/dist/examples/vintasend-medplum-example/src/components/messages/ThreadInbox.tsx +358 -0
  118. package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.module.css +40 -0
  119. package/dist/examples/vintasend-medplum-example/src/components/plandefinition/AddPlanDefinition.tsx +257 -0
  120. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.module.css +7 -0
  121. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.test.tsx +279 -0
  122. package/dist/examples/vintasend-medplum-example/src/components/schedule/CreateVisit.tsx +156 -0
  123. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.module.css +45 -0
  124. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.test.tsx +90 -0
  125. package/dist/examples/vintasend-medplum-example/src/components/spaces/HistoryList.tsx +84 -0
  126. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.module.css +26 -0
  127. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourceBox.tsx +90 -0
  128. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.test.tsx +305 -0
  129. package/dist/examples/vintasend-medplum-example/src/components/spaces/ResourcePanel.tsx +46 -0
  130. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.module.css +262 -0
  131. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.test.tsx +622 -0
  132. package/dist/examples/vintasend-medplum-example/src/components/spaces/SpacesInbox.tsx +286 -0
  133. package/dist/examples/vintasend-medplum-example/src/components/tasks/NewTaskModal.tsx +275 -0
  134. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskAttachmentList.tsx +132 -0
  135. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.module.css +45 -0
  136. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.test.tsx +749 -0
  137. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskBoard.tsx +416 -0
  138. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.test.tsx +278 -0
  139. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailPanel.tsx +133 -0
  140. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.module.css +16 -0
  141. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.test.tsx +255 -0
  142. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskDetailsModal.tsx +203 -0
  143. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFileUpload.tsx +129 -0
  144. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.test.tsx +156 -0
  145. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.tsx +142 -0
  146. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskFilterMenu.utils.ts +28 -0
  147. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.test.tsx +134 -0
  148. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskInputNote.tsx +250 -0
  149. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.module.css +23 -0
  150. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.test.tsx +149 -0
  151. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskListItem.tsx +53 -0
  152. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.test.tsx +68 -0
  153. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskNoteItem.tsx +46 -0
  154. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.test.tsx +555 -0
  155. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskProperties.tsx +170 -0
  156. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.test.tsx +32 -0
  157. package/dist/examples/vintasend-medplum-example/src/components/tasks/TaskSelectEmpty.tsx +34 -0
  158. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.test.tsx +47 -0
  159. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/SimpleTask.tsx +29 -0
  160. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.test.tsx +285 -0
  161. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskPanel.tsx +129 -0
  162. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.test.tsx +455 -0
  163. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskQuestionnaireForm.tsx +167 -0
  164. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.test.tsx +435 -0
  165. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskServiceRequest.tsx +116 -0
  166. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.module.css +38 -0
  167. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.test.tsx +200 -0
  168. package/dist/examples/vintasend-medplum-example/src/components/tasks/encounter/TaskStatusPanel.tsx +84 -0
  169. package/dist/examples/vintasend-medplum-example/src/components/utils.test.ts +176 -0
  170. package/dist/examples/vintasend-medplum-example/src/components/utils.ts +17 -0
  171. package/dist/examples/vintasend-medplum-example/src/config/constants.ts +3 -0
  172. package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.test.tsx +166 -0
  173. package/dist/examples/vintasend-medplum-example/src/hooks/useDebouncedUpdateResource.ts +28 -0
  174. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.test.tsx +94 -0
  175. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounter.ts +11 -0
  176. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.test.tsx +477 -0
  177. package/dist/examples/vintasend-medplum-example/src/hooks/useEncounterChart.ts +191 -0
  178. package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.test.tsx +100 -0
  179. package/dist/examples/vintasend-medplum-example/src/hooks/usePatient.ts +18 -0
  180. package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.test.tsx +379 -0
  181. package/dist/examples/vintasend-medplum-example/src/hooks/useThreadInbox.ts +194 -0
  182. package/dist/examples/vintasend-medplum-example/src/index.css +8 -0
  183. package/dist/examples/vintasend-medplum-example/src/main.tsx +57 -0
  184. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.module.css +6 -0
  185. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.test.tsx +295 -0
  186. package/dist/examples/vintasend-medplum-example/src/pages/SearchPage.tsx +124 -0
  187. package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.test.tsx +77 -0
  188. package/dist/examples/vintasend-medplum-example/src/pages/SignInPage.tsx +22 -0
  189. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.test.tsx +87 -0
  190. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterChartPage.tsx +27 -0
  191. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.module.css +16 -0
  192. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.test.tsx +287 -0
  193. package/dist/examples/vintasend-medplum-example/src/pages/encounter/EncounterModal.tsx +151 -0
  194. package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.test.tsx +519 -0
  195. package/dist/examples/vintasend-medplum-example/src/pages/integrations/DoseSpotFavoritesPage.tsx +179 -0
  196. package/dist/examples/vintasend-medplum-example/src/pages/integrations/FavoriteMedicationsTable.tsx +76 -0
  197. package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.test.tsx +234 -0
  198. package/dist/examples/vintasend-medplum-example/src/pages/integrations/IntegrationsPage.tsx +222 -0
  199. package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.test.tsx +356 -0
  200. package/dist/examples/vintasend-medplum-example/src/pages/labs/OrderLabsPage.tsx +275 -0
  201. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.module.css +8 -0
  202. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.test.tsx +103 -0
  203. package/dist/examples/vintasend-medplum-example/src/pages/messages/MessagesPage.tsx +78 -0
  204. package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.test.tsx +84 -0
  205. package/dist/examples/vintasend-medplum-example/src/pages/patient/CommunicationTab.tsx +82 -0
  206. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.test.tsx +364 -0
  207. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotAdvancedOptions.tsx +149 -0
  208. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.test.tsx +159 -0
  209. package/dist/examples/vintasend-medplum-example/src/pages/patient/DoseSpotTab.tsx +37 -0
  210. package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.test.tsx +140 -0
  211. package/dist/examples/vintasend-medplum-example/src/pages/patient/EditTab.tsx +72 -0
  212. package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.test.tsx +57 -0
  213. package/dist/examples/vintasend-medplum-example/src/pages/patient/ExportTab.tsx +14 -0
  214. package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.test.tsx +241 -0
  215. package/dist/examples/vintasend-medplum-example/src/pages/patient/IntakeFormPage.tsx +710 -0
  216. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.module.css +37 -0
  217. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.test.tsx +428 -0
  218. package/dist/examples/vintasend-medplum-example/src/pages/patient/LabsPage.tsx +334 -0
  219. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.module.css +24 -0
  220. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.test.tsx +154 -0
  221. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.tsx +115 -0
  222. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.test.ts +223 -0
  223. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientPage.utils.ts +89 -0
  224. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.test.tsx +147 -0
  225. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientSearchPage.tsx +79 -0
  226. package/dist/examples/vintasend-medplum-example/src/pages/patient/PatientTabsNavigation.tsx +35 -0
  227. package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.test.tsx +185 -0
  228. package/dist/examples/vintasend-medplum-example/src/pages/patient/TasksTab.tsx +115 -0
  229. package/dist/examples/vintasend-medplum-example/src/pages/patient/TimelineTab.tsx +14 -0
  230. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.test.tsx +170 -0
  231. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceCreatePage.tsx +117 -0
  232. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceDetailPage.tsx +28 -0
  233. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.test.tsx +131 -0
  234. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceEditPage.tsx +65 -0
  235. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.test.tsx +108 -0
  236. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourceHistoryPage.tsx +16 -0
  237. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.module.css +7 -0
  238. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.test.tsx +37 -0
  239. package/dist/examples/vintasend-medplum-example/src/pages/resource/ResourcePage.tsx +44 -0
  240. package/dist/examples/vintasend-medplum-example/src/pages/resource/useResourceType.ts +44 -0
  241. package/dist/examples/vintasend-medplum-example/src/pages/resource/utils.ts +9 -0
  242. package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.test.tsx +302 -0
  243. package/dist/examples/vintasend-medplum-example/src/pages/schedule/SchedulePage.tsx +416 -0
  244. package/dist/examples/vintasend-medplum-example/src/pages/spaces/ChatInput.tsx +91 -0
  245. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.module.css +6 -0
  246. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.test.tsx +102 -0
  247. package/dist/examples/vintasend-medplum-example/src/pages/spaces/SpacesPage.tsx +44 -0
  248. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.module.css +7 -0
  249. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.test.tsx +133 -0
  250. package/dist/examples/vintasend-medplum-example/src/pages/tasks/TasksPage.tsx +91 -0
  251. package/dist/examples/vintasend-medplum-example/src/test-utils/render.tsx +20 -0
  252. package/dist/examples/vintasend-medplum-example/src/test.setup.ts +49 -0
  253. package/dist/examples/vintasend-medplum-example/src/types/encounter.ts +8 -0
  254. package/dist/examples/vintasend-medplum-example/src/types/spaces.ts +10 -0
  255. package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.test.ts +141 -0
  256. package/dist/examples/vintasend-medplum-example/src/utils/chargeitems.ts +59 -0
  257. package/dist/examples/vintasend-medplum-example/src/utils/claims.test.ts +153 -0
  258. package/dist/examples/vintasend-medplum-example/src/utils/claims.ts +65 -0
  259. package/dist/examples/vintasend-medplum-example/src/utils/communication-search.ts +47 -0
  260. package/dist/examples/vintasend-medplum-example/src/utils/coverage.test.ts +48 -0
  261. package/dist/examples/vintasend-medplum-example/src/utils/coverage.ts +33 -0
  262. package/dist/examples/vintasend-medplum-example/src/utils/documentReference.test.ts +102 -0
  263. package/dist/examples/vintasend-medplum-example/src/utils/documentReference.ts +55 -0
  264. package/dist/examples/vintasend-medplum-example/src/utils/encounter.test.ts +169 -0
  265. package/dist/examples/vintasend-medplum-example/src/utils/encounter.ts +261 -0
  266. package/dist/examples/vintasend-medplum-example/src/utils/intake-form.test.ts +154 -0
  267. package/dist/examples/vintasend-medplum-example/src/utils/intake-form.ts +272 -0
  268. package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.test.ts +1137 -0
  269. package/dist/examples/vintasend-medplum-example/src/utils/intake-utils.ts +827 -0
  270. package/dist/examples/vintasend-medplum-example/src/utils/notifications.test.ts +27 -0
  271. package/dist/examples/vintasend-medplum-example/src/utils/notifications.ts +15 -0
  272. package/dist/examples/vintasend-medplum-example/src/utils/spaceMessaging.ts +249 -0
  273. package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.test.ts +450 -0
  274. package/dist/examples/vintasend-medplum-example/src/utils/spacePersistence.ts +147 -0
  275. package/dist/examples/vintasend-medplum-example/src/utils/task-search.ts +63 -0
  276. package/dist/examples/vintasend-medplum-example/src/vite-env.d.ts +3 -0
  277. package/dist/examples/vintasend-medplum-example/tsconfig.bots.json +4 -0
  278. package/dist/examples/vintasend-medplum-example/tsconfig.json +19 -0
  279. package/dist/examples/vintasend-medplum-example/vercel.json +3 -0
  280. package/dist/examples/vintasend-medplum-example/vite.config.ts +44 -0
  281. package/dist/services/notification-backends/base-notification-backend.d.ts +5 -0
  282. package/dist/services/notification-service.js +11 -1
  283. package/dist/services/notification-template-renderers/base-email-template-renderer.d.ts +5 -0
  284. package/package.json +1 -1
@@ -0,0 +1,450 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import type { Communication } from '@medplum/fhirtypes';
4
+ import { MockClient } from '@medplum/mock';
5
+ import { describe, expect, test, beforeEach, vi } from 'vitest';
6
+ import { createConversationTopic, saveMessage, loadConversationMessages, loadRecentTopics } from './spacePersistence';
7
+ import type { Message } from '../types/spaces';
8
+
9
+ describe('spacePersistence', () => {
10
+ let medplum: MockClient;
11
+ let mockProfile: { id: string; resourceType: string };
12
+
13
+ beforeEach(() => {
14
+ medplum = new MockClient();
15
+ mockProfile = {
16
+ id: 'practitioner-123',
17
+ resourceType: 'Practitioner',
18
+ };
19
+ vi.spyOn(medplum, 'getProfile').mockResolvedValue(mockProfile as any);
20
+ });
21
+
22
+ describe('createConversationTopic', () => {
23
+ test('creates conversation topic with title and model', async () => {
24
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({
25
+ resourceType: 'Communication',
26
+ id: 'topic-1',
27
+ status: 'in-progress',
28
+ } as Communication & { id: string });
29
+
30
+ const result = await createConversationTopic(medplum, 'Test Topic', 'gpt-4');
31
+
32
+ expect(createSpy).toHaveBeenCalledWith(
33
+ expect.objectContaining({
34
+ resourceType: 'Communication',
35
+ status: 'in-progress',
36
+ identifier: [
37
+ {
38
+ system: 'http://medplum.com/ai-message',
39
+ value: 'ai-message-topic',
40
+ },
41
+ ],
42
+ sender: { reference: 'Practitioner/practitioner-123' },
43
+ topic: {
44
+ text: 'Test Topic',
45
+ },
46
+ note: [
47
+ {
48
+ text: JSON.stringify({ model: 'gpt-4' }),
49
+ },
50
+ ],
51
+ })
52
+ );
53
+ expect(result).toBeDefined();
54
+ });
55
+
56
+ test('throws error when profile not found', async () => {
57
+ vi.spyOn(medplum, 'getProfile').mockResolvedValue(undefined);
58
+
59
+ await expect(createConversationTopic(medplum, 'Test Topic', 'gpt-4')).rejects.toThrow('Profile not found');
60
+ });
61
+
62
+ test('throws error when profile has no id', async () => {
63
+ vi.spyOn(medplum, 'getProfile').mockResolvedValue({} as any);
64
+
65
+ await expect(createConversationTopic(medplum, 'Test Topic', 'gpt-4')).rejects.toThrow('Profile not found');
66
+ });
67
+ });
68
+
69
+ describe('saveMessage', () => {
70
+ test('saves message with all fields', async () => {
71
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({
72
+ resourceType: 'Communication',
73
+ id: 'message-1',
74
+ status: 'completed',
75
+ } as Communication & { id: string });
76
+
77
+ const message: Message = {
78
+ role: 'user',
79
+ content: 'Hello, world!',
80
+ tool_calls: [{ id: 'call-1', type: 'function' }],
81
+ tool_call_id: 'call-1',
82
+ resources: ['Patient/123'],
83
+ };
84
+
85
+ const result = await saveMessage(medplum, 'topic-1', message, 1);
86
+
87
+ expect(createSpy).toHaveBeenCalledWith(
88
+ expect.objectContaining({
89
+ resourceType: 'Communication',
90
+ status: 'completed',
91
+ identifier: [
92
+ {
93
+ system: 'http://medplum.com/ai-message',
94
+ value: 'ai-message',
95
+ },
96
+ ],
97
+ partOf: [
98
+ {
99
+ reference: 'Communication/topic-1',
100
+ },
101
+ ],
102
+ payload: [
103
+ {
104
+ contentString: JSON.stringify({
105
+ role: 'user',
106
+ content: 'Hello, world!',
107
+ tool_calls: [{ id: 'call-1', type: 'function' }],
108
+ tool_call_id: 'call-1',
109
+ resources: ['Patient/123'],
110
+ sequenceNumber: 1,
111
+ }),
112
+ },
113
+ ],
114
+ })
115
+ );
116
+ expect(result).toBeDefined();
117
+ });
118
+
119
+ test('saves message with minimal fields', async () => {
120
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({
121
+ resourceType: 'Communication',
122
+ id: 'message-2',
123
+ status: 'completed',
124
+ } as Communication & { id: string });
125
+
126
+ const message: Message = {
127
+ role: 'assistant',
128
+ content: 'Response',
129
+ };
130
+
131
+ const result = await saveMessage(medplum, 'topic-2', message, 2);
132
+
133
+ expect(createSpy).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ payload: [
136
+ {
137
+ contentString: JSON.stringify({
138
+ role: 'assistant',
139
+ content: 'Response',
140
+ tool_calls: undefined,
141
+ tool_call_id: undefined,
142
+ resources: undefined,
143
+ sequenceNumber: 2,
144
+ }),
145
+ },
146
+ ],
147
+ })
148
+ );
149
+ expect(result).toBeDefined();
150
+ });
151
+
152
+ test('saves message with null content', async () => {
153
+ const createSpy = vi.spyOn(medplum, 'createResource').mockResolvedValue({
154
+ resourceType: 'Communication',
155
+ id: 'message-3',
156
+ status: 'completed',
157
+ } as Communication & { id: string });
158
+
159
+ const message: Message = {
160
+ role: 'tool',
161
+ content: null,
162
+ tool_call_id: 'call-1',
163
+ };
164
+
165
+ await saveMessage(medplum, 'topic-3', message, 3);
166
+
167
+ const call = createSpy.mock.calls[0][0] as Communication;
168
+ const payload = JSON.parse(call.payload?.[0]?.contentString ?? '{}');
169
+ expect(payload.content).toBeNull();
170
+ expect(payload.role).toBe('tool');
171
+ expect(payload.tool_call_id).toBe('call-1');
172
+ expect(payload.sequenceNumber).toBe(3);
173
+ });
174
+ });
175
+
176
+ describe('loadConversationMessages', () => {
177
+ test('loads messages from communications', async () => {
178
+ const mockCommunications: (Communication & { id: string })[] = [
179
+ {
180
+ resourceType: 'Communication',
181
+ id: 'comm-1',
182
+ status: 'completed',
183
+ payload: [
184
+ {
185
+ contentString: JSON.stringify({
186
+ role: 'user',
187
+ content: 'Hello',
188
+ sequenceNumber: 1,
189
+ }),
190
+ },
191
+ ],
192
+ },
193
+ {
194
+ resourceType: 'Communication',
195
+ id: 'comm-2',
196
+ status: 'completed',
197
+ payload: [
198
+ {
199
+ contentString: JSON.stringify({
200
+ role: 'assistant',
201
+ content: 'Hi there',
202
+ sequenceNumber: 2,
203
+ }),
204
+ },
205
+ ],
206
+ },
207
+ ];
208
+
209
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockCommunications as any);
210
+
211
+ const messages = await loadConversationMessages(medplum, 'topic-1');
212
+
213
+ expect(medplum.searchResources).toHaveBeenCalledWith('Communication', {
214
+ 'part-of': 'Communication/topic-1',
215
+ _sort: '_lastUpdated',
216
+ });
217
+ expect(messages).toEqual([
218
+ {
219
+ role: 'user',
220
+ content: 'Hello',
221
+ },
222
+ {
223
+ role: 'assistant',
224
+ content: 'Hi there',
225
+ },
226
+ ]);
227
+ });
228
+
229
+ test('handles messages with tool_calls and resources', async () => {
230
+ const mockCommunications: (Communication & { id: string })[] = [
231
+ {
232
+ resourceType: 'Communication',
233
+ id: 'comm-1',
234
+ status: 'completed',
235
+ payload: [
236
+ {
237
+ contentString: JSON.stringify({
238
+ role: 'assistant',
239
+ content: 'I can help',
240
+ tool_calls: [{ id: 'call-1', type: 'function' }],
241
+ resources: ['Patient/123'],
242
+ sequenceNumber: 1,
243
+ }),
244
+ },
245
+ ],
246
+ },
247
+ ];
248
+
249
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockCommunications as any);
250
+
251
+ const messages = await loadConversationMessages(medplum, 'topic-1');
252
+
253
+ expect(messages).toEqual([
254
+ {
255
+ role: 'assistant',
256
+ content: 'I can help',
257
+ tool_calls: [{ id: 'call-1', type: 'function' }],
258
+ resources: ['Patient/123'],
259
+ },
260
+ ]);
261
+ });
262
+
263
+ test('handles messages without sequenceNumber', async () => {
264
+ const mockCommunications: (Communication & { id: string })[] = [
265
+ {
266
+ resourceType: 'Communication',
267
+ id: 'comm-1',
268
+ status: 'completed',
269
+ payload: [
270
+ {
271
+ contentString: JSON.stringify({
272
+ role: 'user',
273
+ content: 'Test',
274
+ }),
275
+ },
276
+ ],
277
+ },
278
+ ];
279
+
280
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockCommunications as any);
281
+
282
+ const messages = await loadConversationMessages(medplum, 'topic-1');
283
+
284
+ expect(messages).toEqual([
285
+ {
286
+ role: 'user',
287
+ content: 'Test',
288
+ },
289
+ ]);
290
+ });
291
+
292
+ test('skips communications without payload', async () => {
293
+ const mockCommunications: (Communication & { id: string })[] = [
294
+ {
295
+ resourceType: 'Communication',
296
+ id: 'comm-1',
297
+ status: 'completed',
298
+ payload: [
299
+ {
300
+ contentString: JSON.stringify({
301
+ role: 'user',
302
+ content: 'Hello',
303
+ sequenceNumber: 1,
304
+ }),
305
+ },
306
+ ],
307
+ },
308
+ {
309
+ resourceType: 'Communication',
310
+ id: 'comm-2',
311
+ status: 'completed',
312
+ // No payload
313
+ },
314
+ {
315
+ resourceType: 'Communication',
316
+ id: 'comm-3',
317
+ status: 'completed',
318
+ payload: [], // Empty payload
319
+ },
320
+ ];
321
+
322
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockCommunications as any);
323
+
324
+ const messages = await loadConversationMessages(medplum, 'topic-1');
325
+
326
+ expect(messages).toEqual([
327
+ {
328
+ role: 'user',
329
+ content: 'Hello',
330
+ },
331
+ ]);
332
+ });
333
+
334
+ test('throws error when JSON parsing fails with Error instance', async () => {
335
+ const mockCommunications: (Communication & { id: string })[] = [
336
+ {
337
+ resourceType: 'Communication',
338
+ id: 'comm-1',
339
+ status: 'completed',
340
+ payload: [
341
+ {
342
+ contentString: 'invalid json{',
343
+ },
344
+ ],
345
+ },
346
+ ];
347
+
348
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockCommunications as any);
349
+
350
+ await expect(loadConversationMessages(medplum, 'topic-1')).rejects.toThrow('Failed to parse message');
351
+ });
352
+
353
+ test('throws error when JSON parsing fails with non-Error', async () => {
354
+ const mockCommunications: (Communication & { id: string })[] = [
355
+ {
356
+ resourceType: 'Communication',
357
+ id: 'comm-1',
358
+ status: 'completed',
359
+ payload: [
360
+ {
361
+ contentString: 'test',
362
+ },
363
+ ],
364
+ },
365
+ ];
366
+
367
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockCommunications as any);
368
+ // Mock JSON.parse to throw a non-Error object
369
+ const originalParse = JSON.parse;
370
+ vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
371
+ throw new Error('string error'); // Throw Error instead of string
372
+ });
373
+
374
+ await expect(loadConversationMessages(medplum, 'topic-1')).rejects.toThrow('Failed to parse message');
375
+
376
+ JSON.parse = originalParse;
377
+ });
378
+
379
+ test('handles empty communications array', async () => {
380
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([] as any);
381
+
382
+ const messages = await loadConversationMessages(medplum, 'topic-1');
383
+
384
+ expect(messages).toEqual([]);
385
+ });
386
+ });
387
+
388
+ describe('loadRecentTopics', () => {
389
+ test('loads recent topics with default limit', async () => {
390
+ const mockTopics: (Communication & { id: string })[] = [
391
+ {
392
+ resourceType: 'Communication',
393
+ id: 'topic-1',
394
+ status: 'in-progress',
395
+ topic: { text: 'Topic 1' },
396
+ },
397
+ {
398
+ resourceType: 'Communication',
399
+ id: 'topic-2',
400
+ status: 'in-progress',
401
+ topic: { text: 'Topic 2' },
402
+ },
403
+ ];
404
+
405
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockTopics as any);
406
+
407
+ const topics = await loadRecentTopics(medplum);
408
+
409
+ expect(medplum.getProfile).toHaveBeenCalled();
410
+ expect(medplum.searchResources).toHaveBeenCalledWith('Communication', {
411
+ identifier: 'http://medplum.com/ai-message|ai-message-topic',
412
+ sender: 'Practitioner/practitioner-123',
413
+ _sort: '-_lastUpdated',
414
+ _count: '10',
415
+ });
416
+ expect(topics).toEqual(mockTopics);
417
+ });
418
+
419
+ test('loads recent topics with custom limit', async () => {
420
+ const mockTopics: (Communication & { id: string })[] = [
421
+ {
422
+ resourceType: 'Communication',
423
+ id: 'topic-1',
424
+ status: 'in-progress',
425
+ topic: { text: 'Topic 1' },
426
+ },
427
+ ];
428
+
429
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue(mockTopics as any);
430
+
431
+ const topics = await loadRecentTopics(medplum, 5);
432
+
433
+ expect(medplum.searchResources).toHaveBeenCalledWith('Communication', {
434
+ identifier: 'http://medplum.com/ai-message|ai-message-topic',
435
+ sender: 'Practitioner/practitioner-123',
436
+ _sort: '-_lastUpdated',
437
+ _count: '5',
438
+ });
439
+ expect(topics).toEqual(mockTopics);
440
+ });
441
+
442
+ test('handles empty topics array', async () => {
443
+ vi.spyOn(medplum, 'searchResources').mockResolvedValue([] as any);
444
+
445
+ const topics = await loadRecentTopics(medplum);
446
+
447
+ expect(topics).toEqual([]);
448
+ });
449
+ });
450
+ });
@@ -0,0 +1,147 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import type { Communication } from '@medplum/fhirtypes';
4
+ import type { MedplumClient, ProfileResource } from '@medplum/core';
5
+ import type { Message } from '../types/spaces';
6
+ import { createReference, getReferenceString } from '@medplum/core';
7
+
8
+ /**
9
+ * Creates a new conversation topic (main Communication resource)
10
+ * @param medplum - The Medplum client instance
11
+ * @param title - The title of the conversation
12
+ * @param model - The AI model being used
13
+ * @returns The created Communication resource
14
+ */
15
+ export async function createConversationTopic(
16
+ medplum: MedplumClient,
17
+ title: string,
18
+ model: string
19
+ ): Promise<Communication> {
20
+ const profile = await medplum.getProfile();
21
+ if (!profile?.id) {
22
+ throw new Error('Profile not found');
23
+ }
24
+
25
+ const topic: Communication = {
26
+ resourceType: 'Communication',
27
+ status: 'in-progress',
28
+ identifier: [
29
+ {
30
+ system: 'http://medplum.com/ai-message',
31
+ value: 'ai-message-topic',
32
+ },
33
+ ],
34
+ sender: createReference(profile),
35
+ topic: {
36
+ text: title,
37
+ },
38
+ note: [
39
+ {
40
+ text: JSON.stringify({ model }),
41
+ },
42
+ ],
43
+ };
44
+
45
+ return medplum.createResource(topic);
46
+ }
47
+
48
+ /**
49
+ * Saves a message as a Communication resource linked to the topic
50
+ * @param medplum - The Medplum client instance
51
+ * @param topicId - The ID of the conversation topic
52
+ * @param message - The message to save
53
+ * @param sequenceNumber - The sequence number of the message
54
+ * @returns The created Communication resource
55
+ */
56
+ export async function saveMessage(
57
+ medplum: MedplumClient,
58
+ topicId: string,
59
+ message: Message,
60
+ sequenceNumber: number
61
+ ): Promise<Communication> {
62
+ const communication: Communication = {
63
+ resourceType: 'Communication',
64
+ status: 'completed',
65
+ identifier: [
66
+ {
67
+ system: 'http://medplum.com/ai-message',
68
+ value: 'ai-message',
69
+ },
70
+ ],
71
+ partOf: [
72
+ {
73
+ reference: `Communication/${topicId}`,
74
+ },
75
+ ],
76
+ payload: [
77
+ {
78
+ contentString: JSON.stringify({
79
+ role: message.role,
80
+ content: message.content,
81
+ tool_calls: message.tool_calls,
82
+ tool_call_id: message.tool_call_id,
83
+ resources: message.resources,
84
+ sequenceNumber,
85
+ }),
86
+ },
87
+ ],
88
+ };
89
+
90
+ return medplum.createResource(communication);
91
+ }
92
+
93
+ /**
94
+ * Loads the last messages for a conversation topic
95
+ * @param medplum - The Medplum client instance
96
+ * @param topicId - The ID of the conversation topic
97
+ * @returns Array of messages
98
+ */
99
+ export async function loadConversationMessages(medplum: MedplumClient, topicId: string): Promise<Message[]> {
100
+ const communications = await medplum.searchResources('Communication', {
101
+ 'part-of': `Communication/${topicId}`,
102
+ _sort: '_lastUpdated',
103
+ });
104
+
105
+ const messages: { message: Message; sequenceNumber: number }[] = [];
106
+
107
+ for (const comm of communications) {
108
+ if (comm.payload?.[0]?.contentString) {
109
+ try {
110
+ const data = JSON.parse(comm.payload[0].contentString);
111
+ messages.push({
112
+ message: {
113
+ role: data.role,
114
+ content: data.content,
115
+ tool_calls: data.tool_calls,
116
+ tool_call_id: data.tool_call_id,
117
+ resources: data.resources,
118
+ },
119
+ sequenceNumber: data.sequenceNumber || 0,
120
+ });
121
+ } catch (error) {
122
+ if (error instanceof Error) {
123
+ throw new Error(`Failed to parse message: ${error.message}`);
124
+ }
125
+ throw new Error(`Failed to parse message: ${String(error)}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ return messages.map((m) => m.message);
131
+ }
132
+
133
+ /**
134
+ * Loads recent conversation topics
135
+ * @param medplum - The Medplum client instance
136
+ * @param limit - Maximum number of topics to return
137
+ * @returns Array of conversation topic Communications
138
+ */
139
+ export async function loadRecentTopics(medplum: MedplumClient, limit = 10): Promise<Communication[]> {
140
+ const profile = await medplum.getProfile();
141
+ return medplum.searchResources('Communication', {
142
+ identifier: 'http://medplum.com/ai-message|ai-message-topic',
143
+ sender: getReferenceString(profile as ProfileResource),
144
+ _sort: '-_lastUpdated',
145
+ _count: String(limit),
146
+ });
147
+ }
@@ -0,0 +1,63 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { parseSearchRequest } from '@medplum/core';
4
+ import type { SearchRequest, Filter } from '@medplum/core';
5
+
6
+ export interface NormalizeTaskSearchOptions {
7
+ /**
8
+ * Additional filters to apply to the search request.
9
+ * These filters will be merged with existing filters, replacing any with the same code.
10
+ */
11
+ additionalFilters?: Filter[];
12
+ }
13
+
14
+ export interface NormalizeTaskSearchResult {
15
+ /**
16
+ * The normalized search request with required fields set.
17
+ */
18
+ normalizedSearch: SearchRequest;
19
+ /**
20
+ * Whether navigation is needed to update the URL with normalized parameters.
21
+ */
22
+ needsNavigation: boolean;
23
+ }
24
+
25
+ /**
26
+ * Normalizes a task search request by ensuring required fields are present.
27
+ * Checks for _lastUpdated sort rule, count, and total parameters.
28
+ *
29
+ * @param locationPathname - The pathname from useLocation()
30
+ * @param locationSearch - The search string from useLocation()
31
+ * @param options - Optional configuration for additional filters
32
+ * @returns Normalized search request and whether navigation is needed
33
+ */
34
+ export function normalizeTaskSearch(
35
+ locationPathname: string,
36
+ locationSearch: string,
37
+ options?: NormalizeTaskSearchOptions
38
+ ): NormalizeTaskSearchResult {
39
+ const parsedSearch = parseSearchRequest(locationPathname + locationSearch);
40
+ const lastUpdatedSortRule = parsedSearch.sortRules?.find((rule) => rule.code === '_lastUpdated');
41
+
42
+ let filters = parsedSearch.filters || [];
43
+ if (options?.additionalFilters) {
44
+ const additionalFilterCodes = new Set(options.additionalFilters.map((f) => f.code));
45
+ filters = filters.filter((f) => !additionalFilterCodes.has(f.code));
46
+ filters = [...filters, ...options.additionalFilters];
47
+ }
48
+
49
+ const normalizedSearch: SearchRequest = {
50
+ ...parsedSearch,
51
+ filters,
52
+ sortRules: lastUpdatedSortRule ? parsedSearch.sortRules : [{ code: '_lastUpdated', descending: true }],
53
+ count: parsedSearch.count || 20,
54
+ total: parsedSearch.total || 'accurate',
55
+ };
56
+
57
+ const needsNavigation = !lastUpdatedSortRule || !parsedSearch.count || !parsedSearch.total;
58
+
59
+ return {
60
+ normalizedSearch,
61
+ needsNavigation,
62
+ };
63
+ }
@@ -0,0 +1,3 @@
1
+ // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /// <reference types="vite/client" />
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["dist", "**/*.test.ts", "**/*.test.tsx", "**/test.setup.ts"]
4
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "esnext",
13
+ "moduleResolution": "bundler",
14
+ "resolveJsonModule": true,
15
+ "noEmit": true,
16
+ "jsx": "react-jsx"
17
+ },
18
+ "exclude": ["dist"]
19
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }]
3
+ }