Skip to content

Commit

Permalink
Migrated Scribe to a CARE App + Context (#8469)
Browse files Browse the repository at this point in the history
  • Loading branch information
shivankacker authored Nov 13, 2024
1 parent 7cb54b3 commit 5bb46fa
Show file tree
Hide file tree
Showing 27 changed files with 507 additions and 1,415 deletions.
6 changes: 3 additions & 3 deletions cypress/pageobject/Patient/PatientLogupdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ class PatientLogupdate {
}

typePulse(pulse: string) {
cy.typeAndSelectOption("#pulse", pulse);
cy.get("#pulse").click().type(pulse);
}

typeTemperature(temperature: string) {
cy.get("#temperature").click().type(temperature);
}

typeRespiratory(respiratory: string) {
cy.typeAndSelectOption("#resp", respiratory);
cy.get("#resp").click().type(respiratory);
}

typeSpo2(spo: string) {
cy.typeAndSelectOption("#ventilator_spo2", spo);
cy.get("#ventilator_spo2").click().type(spo);
}

selectRhythm(rhythm: string) {
Expand Down
17 changes: 17 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@
"abha_number_exists_description": "There is an ABHA Number already linked with the given Aadhaar Number, Do you want to create a new ABHA Address?",
"abha_number_linked_successfully": "ABHA Number has been linked successfully.",
"abha_profile": "ABHA Profile",
"accept": "Accept",
"accept_all": "Accept All",
"access_level": "Access Level",
"action_irreversible": "This action is irreversible",
"active": "Active",
Expand Down Expand Up @@ -342,6 +344,7 @@
"auth_method_unsupported": "This authentication method is not supported, please try a different method",
"authorize_shift_delete": "Authorize shift delete",
"auto_generated_for_care": "Auto Generated for Care",
"autofilled_fields": "Autofilled Fields",
"available_features": "Available Features",
"available_in": "Available in",
"average_weekly_working_hours": "Average weekly working hours",
Expand Down Expand Up @@ -500,6 +503,8 @@
"continue_watching": "Continue watching",
"contribute_github": "Contribute on Github",
"copied_to_clipboard": "Copied to clipboard",
"copilot_thinking": "Copilot is thinking...",
"could_not_autofill": "We could not autofill any fields from what you said",
"countries_travelled": "Countries travelled",
"covid_19_cat_gov": "Covid_19 Clinical Category as per Govt. of Kerala guideline (A/B/C)",
"covid_19_death_reporting_form_1": "Covid-19 Death Reporting : Form 1",
Expand Down Expand Up @@ -722,6 +727,7 @@
"health_facility__registered_2": "No Action Required",
"health_facility__registered_3": "Registered",
"health_facility__validation__hf_id_required": "Health Facility Id is required",
"hearing": "We are hearing you...",
"help_confirmed": "There is sufficient diagnostic and/or clinical evidence to treat this as a confirmed condition.",
"help_differential": "One of a set of potential (and typically mutually exclusive) diagnoses asserted to further guide the diagnostic process and preliminary treatment.",
"help_entered-in-error": "The statement was entered in error and is not valid.",
Expand Down Expand Up @@ -1041,6 +1047,7 @@
"prn_prescriptions": "PRN Prescriptions",
"procedure_suggestions": "Procedure Suggestions",
"procedures_select_placeholder": "Select procedures to add details",
"process_transcript": "Process Again",
"profile": "Profile",
"provisional": "Provisional",
"qualification": "Qualification",
Expand Down Expand Up @@ -1068,6 +1075,7 @@
"refuted": "Refuted",
"register_hospital": "Register Hospital",
"register_page_title": "Register As Hospital Administrator",
"reject": "Reject",
"reload": "Reload",
"remove": "Remove",
"rename": "Rename",
Expand Down Expand Up @@ -1097,6 +1105,7 @@
"result_on": "Result on",
"resume": "Resume",
"retake": "Retake",
"retake_recording": "Retake Recording",
"return_to_care": "Return to CARE",
"return_to_login": "Return to Login",
"return_to_password_reset": "Return to Password Reset",
Expand All @@ -1117,6 +1126,8 @@
"save_and_continue": "Save and Continue",
"save_investigation": "Save Investigation",
"scan_asset_qr": "Scan Asset QR!",
"scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}",
"scribe_error": "Could not autofill fields",
"search_by_username": "Search by username",
"search_for_facility": "Search for Facility",
"search_icd11_placeholder": "Search for ICD-11 Diagnoses",
Expand Down Expand Up @@ -1181,9 +1192,11 @@
"staff_list": "Staff List",
"start_datetime": "Start Date/Time",
"start_dosage": "Start Dosage",
"start_review": "Start Review",
"state": "State",
"status": "Status",
"stop": "Stop",
"stop_recording": "Stop Recording",
"stream_stop_due_to_inativity": "The live feed will stop streaming due to inactivity",
"stream_stopped_due_to_inativity": "The live feed has stopped streaming due to inactivity",
"stream_uuid": "Stream UUID",
Expand All @@ -1200,6 +1213,7 @@
"support": "Support",
"switch": "Switch",
"switch_camera_is_not_available": "Switch camera is not available.",
"symptoms": "Symptoms",
"systolic": "Systolic",
"tachycardia": "Tachycardia",
"target_dosage": "Target Dosage",
Expand All @@ -1212,6 +1226,8 @@
"total_beds": "Total Beds",
"total_staff": "Total Staff",
"total_users": "Total Users",
"transcript_edit_info": "You can update this if we made an error",
"transcript_information": "This is what we heard",
"transfer_in_progress": "TRANSFER IN PROGRESS",
"transfer_to_receiving_facility": "Transfer to receiving facility",
"travel_within_last_28_days": "Domestic/international Travel (within last 28 days)",
Expand Down Expand Up @@ -1301,6 +1317,7 @@
"vitals": "Vitals",
"vitals_monitor": "Vitals Monitor",
"vitals_present": "Vitals Monitor present",
"voice_autofill": "Voice Autofill",
"ward": "Ward",
"warranty_amc_expiry": "Warranty / AMC Expiry",
"what_facility_assign_the_patient_to": "What facility would you like to assign the patient to",
Expand Down
29 changes: 28 additions & 1 deletion src/CAREUI/interactive/KeyboardShortcut.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Fragment } from "react/jsx-runtime";
import useKeyboardShortcut from "use-keyboard-shortcut";

import { classNames, isAppleDevice } from "@/Utils/utils";
import { classNames, isAppleDevice } from "../../Utils/utils";

interface Props {
children?: React.ReactNode;
Expand Down Expand Up @@ -70,3 +71,29 @@ const SHORTCUT_KEY_MAP = {
ArrowLeft: "←",
ArrowRight: "→",
} as Record<string, string>;

export function KeyboardShortcutKey(props: {
shortcut: string[];
useShortKeys?: boolean;
}) {
const { shortcut, useShortKeys } = props;

return (
<div className="hidden shrink-0 items-center md:flex">
{shortcut.map((key, idx, keys) => (
<Fragment key={idx}>
<kbd className="relative z-10 flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md border border-b-4 border-secondary-400 bg-secondary-100 px-2 text-xs text-black">
{SHORTCUT_KEY_MAP[key]
? useShortKeys
? SHORTCUT_KEY_MAP[key][0]
: SHORTCUT_KEY_MAP[key]
: key}
</kbd>
{idx !== keys.length - 1 && (
<span className="px-1 text-zinc-300/60"> + </span>
)}
</Fragment>
))}
</div>
);
}
5 changes: 4 additions & 1 deletion src/Routers/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ export default function AppRouter() {
id="pages"
className="flex-1 overflow-y-scroll bg-gray-100 pb-4 focus:outline-none md:py-0"
>
<div className="max-w-8xl mx-auto mt-4 min-h-[96vh] rounded-lg border bg-gray-50 p-3 shadow">
<div
className="max-w-8xl mx-auto mt-4 min-h-[96vh] rounded-lg border bg-gray-50 p-3 shadow"
data-cui-page
>
{pages}
</div>
</main>
Expand Down
31 changes: 0 additions & 31 deletions src/Utils/request/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ import {
CreateFileResponse,
FileUploadModel,
} from "@/components/Patient/models";
import { ScribeModel } from "@/components/Scribe/Scribe";
import {
SkillModel,
SkillObjectModel,
Expand Down Expand Up @@ -111,36 +110,6 @@ export interface LoginCredentials {
}

const routes = {
createScribe: {
path: "/api/care_scribe/scribe/",
method: "POST",
TReq: Type<ScribeModel>(),
TRes: Type<ScribeModel>(),
},
getScribe: {
path: "/api/care_scribe/scribe/{external_id}/",
method: "GET",
TRes: Type<ScribeModel>(),
},
updateScribe: {
path: "/api/care_scribe/scribe/{external_id}/",
method: "PUT",
TReq: Type<ScribeModel>(),
TRes: Type<ScribeModel>(),
},
createScribeFileUpload: {
path: "/api/care_scribe/scribe_file/",
method: "POST",
TBody: Type<CreateFileRequest>(),
TRes: Type<CreateFileResponse>(),
},
editScribeFileUpload: {
path: "/api/care_scribe/scribe_file/{id}/?file_type={fileType}&associating_id={associatingId}",
method: "PATCH",
TBody: Type<Partial<FileUploadModel>>(),
TRes: Type<FileUploadModel>(),
},

// Auth Endpoints
login: {
path: "/api/v1/auth/login/",
Expand Down
16 changes: 14 additions & 2 deletions src/Utils/useSegmentedRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const useSegmentedRecording = () => {
const [recorder, setRecorder] = useState<MediaRecorder | null>(null);
const [audioBlobs, setAudioBlobs] = useState<Blob[]>([]);
const [restart, setRestart] = useState(false);
const [microphoneAccess, setMicrophoneAccess] = useState(false); // New state
const { t } = useTranslation();

const bufferInterval = 1 * 1000;
Expand All @@ -25,6 +26,7 @@ const useSegmentedRecording = () => {
requestRecorder().then(
(newRecorder) => {
setRecorder(newRecorder);
setMicrophoneAccess(true); // Set access to true on success
if (restart) {
setIsRecording(true);
}
Expand All @@ -34,6 +36,7 @@ const useSegmentedRecording = () => {
msg: t("audio__permission_message"),
});
setIsRecording(false);
setMicrophoneAccess(false); // Set access to false on failure
},
);
}
Expand Down Expand Up @@ -98,8 +101,16 @@ const useSegmentedRecording = () => {
return () => recorder.removeEventListener("dataavailable", handleData);
}, [recorder, isRecording, bufferInterval, audioBlobs, restart]);

const startRecording = () => {
setIsRecording(true);
const startRecording = async () => {
try {
const newRecorder = await requestRecorder();
setRecorder(newRecorder);
setMicrophoneAccess(true);
setIsRecording(true);
} catch (error) {
setMicrophoneAccess(false);
throw new Error("Microphone access denied");
}
};

const stopRecording = () => {
Expand All @@ -116,6 +127,7 @@ const useSegmentedRecording = () => {
stopRecording,
resetRecording,
audioBlobs,
microphoneAccess, // Return microphoneAccess
};
};

Expand Down
55 changes: 55 additions & 0 deletions src/Utils/useValueInjectionObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from "react";

/**
* A custom React hook that observes changes to a specific attribute of a DOM element
* and returns a new value when the attribute changes. It is primarily useful
* for detecting updates to the value of a custom CUI component through layers where
* the component's state cannot be determined or mutated (example. CARE Scribe).
*
* @template T
* @param {Object} options - Configuration options for the observer.
* @param {HTMLElement | null} options.targetElement - The DOM element to observe for attribute changes.
* @param {string} [options.attribute="value"] - The name of the attribute to observe (default is "value").
*
* @example
* const targetElement = document.getElementById('my-input');
* const DOMValue = useValueInjectionObserver({
* targetElement: targetElement,
* attribute: 'value',
* });
*
* @returns {unknown} This hook returns the current value of the attribute.
*/
export function useValueInjectionObserver<T = unknown>(options: {
targetElement: HTMLElement | null;
attribute?: string;
}) {
const { targetElement, attribute = "value" } = options;
const [value, setValue] = useState<T>();

useEffect(() => {
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === attribute
) {
const newValue = JSON.parse(
targetElement?.getAttribute(attribute) || '""',
);
setValue(newValue);
}
});
});

const config = {
attributes: true,
attributeFilter: [attribute],
};

targetElement && observer.observe(targetElement, config);
return () => observer.disconnect();
}, [targetElement]);

return value;
}
Loading

0 comments on commit 5bb46fa

Please sign in to comment.