Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scribe for Questionnaire #9584

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ ESLINT_NO_DEV_ERRORS=true
CARE_CDN_URL="https://egov-s3-facility-10bedicu.s3.amazonaws.com https://egov-s3-patient-data-10bedicu.s3.amazonaws.com http://localhost:4566"
REACT_ALLOWED_LOCALES="en,hi,ta,ml,mr,kn"

REACT_ENABLED_APPS=""
REACT_ENABLED_APPS=localhost:4173/ohcnetwork/care_scribe_fe

REACT_KERALA_GEO_ID=b04b9efa-5972-419a-892d-26169c1eac8a
19 changes: 4 additions & 15 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,6 @@
"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",
"available_time_slots": "Available Time Slots",
Expand Down Expand Up @@ -606,9 +605,7 @@
"continue_watching": "Continue watching",
"contribute_github": "Contribute on Github",
"copied_to_clipboard": "Copied to clipboard",
"copilot_thinking": "Copilot is thinking...",
"copy_phone_number": "Copy Phone Number",
"could_not_autofill": "We could not autofill any fields from what you said",
"could_not_load_page": "We are facing some difficulties showing the Page you were looking for. Our Engineers have been notified and we'll make sure that this is resolved on the fly!",
"countries_travelled": "Countries travelled",
"covid_19_cat_gov": "Covid_19 Clinical Category as per Govt. of Kerala guideline (A/B/C)",
Expand Down Expand Up @@ -1433,7 +1430,6 @@
"prn_prescriptions": "PRN Prescriptions",
"procedure_suggestions": "Procedure Suggestions",
"procedures_select_placeholder": "Select procedures to add details",
"process_transcript": "Process Again",
"professional_info": "Professional Information",
"professional_info_note": "View or update user's professional information",
"professional_info_note_self": "View or update your professional information",
Expand All @@ -1444,8 +1440,8 @@
"qualification": "Qualification",
"qualification_required": "Qualification is required",
"quantity_approved": "Quantity Approved",
"quantity_required": "Quantity Required",
"quantity_requested": "Quantity Requested",
"quantity_required": "Quantity Required",
"raise_consent_request": "Raise a consent request to fetch patient records over ABDM",
"ration_card__APL": "APL",
"ration_card__BPL": "BPL",
Expand Down Expand Up @@ -1552,8 +1548,6 @@
"schedule_calendar": "Schedule Calendar",
"schedule_information": "Schedule Information",
"scheduled": "Scheduled",
"scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}",
"scribe_error": "Could not autofill fields",
"search": "Search",
"search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number",
"search_by_emergency_phone_number": "Search by Emergency Phone Number",
Expand Down Expand Up @@ -1645,7 +1639,6 @@
"start_consultation": "Start Consultation",
"start_datetime": "Start Date/Time",
"start_dosage": "Start Dosage",
"start_review": "Start Review",
"state": "State",
"status": "Status",
"stop": "Stop",
Expand Down Expand Up @@ -1676,10 +1669,10 @@
"test_type": "Type of test done",
"tested_on": "Tested on",
"thank_you_for_choosing": "Thank you for choosing our care service",
"time": "Time",
"time_slot": "Time Slot",
"the_request_for_resources_placed_by_yourself_is": "The request for resource (details below) placed by yourself is",
"third_party_software_licenses": "Third Party Software Licenses",
"time": "Time",
"time_slot": "Time Slot",
"title_of_request": "Title of Request",
"titrate_dosage": "Titrate Dosage",
"to": "to",
Expand All @@ -1694,9 +1687,6 @@
"total_patients": "Total Patients",
"total_staff": "Total Staff",
"total_users": "Total Users",
"transcribe_again": "Transcribe Again",
"transcript_edit_info": "You can update this if we made an error",
"transcript_information": "This is what we heard",
"transfer_allowed": "Transfer Allowed",
"transfer_blocked": "Transfer Blocked",
"transfer_in_progress": "TRANSFER IN PROGRESS",
Expand Down Expand Up @@ -1844,7 +1834,6 @@
"vitals": "Vitals",
"vitals_monitor": "Vitals Monitor",
"vitals_present": "Vitals Monitor present",
"voice_autofill": "Voice Autofill",
"volunteer_assigned": "Volunteer assigned successfully",
"volunteer_contact": "Volunteer Contact",
"volunteer_contact_detail": "Provide the name and contact details of a volunteer who can assist the patient in emergencies. This should be someone outside the family.",
Expand All @@ -1871,4 +1860,4 @@
"you_need_at_least_a_location_to_create_an_assest": "You need at least a location to create an assest.",
"zoom_in": "Zoom In",
"zoom_out": "Zoom Out"
}
}
40 changes: 40 additions & 0 deletions src/Utils/scribe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback, useState } from "react";

import { useValueInjection } from "./useValueInjectionObserver";

export default function ScribeStructuredInput<T = unknown>(props: {
name: string;
prompt: string;
example: unknown;
value: T;
onChange: (value: T | undefined) => void;
className?: string;
children: React.ReactNode;
}) {
const { name, prompt, example, value, onChange, children, className } = props;
const [element, setElement] = useState<HTMLElement | null>(null);

const callbackRef = useCallback(
(node: HTMLElement | null) => setElement(node),
[],
);

useValueInjection<T>({
targetElement: element,
onChange,
});

return (
<div
ref={callbackRef}
data-scribe-structured-input
data-scribe-name={name}
data-scribe-prompt={prompt}
data-scribe-example={JSON.stringify(example)}
data-scribe-value={JSON.stringify(value)}
className={className}
>
{children}
</div>
);
}
19 changes: 19 additions & 0 deletions src/Utils/useValueInjectionObserver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,22 @@ export function useValueInjectionObserver<T = unknown>(options: {

return value;
}

export function useValueInjection<T = unknown>(options: {
targetElement: HTMLElement | null;
attribute?: string;
onChange: (value: T | undefined) => void;
}) {
const { targetElement, attribute = "data-scribe-value", onChange } = options;

const domValue = useValueInjectionObserver<T>({
targetElement,
attribute,
});

useEffect(() => {
onChange(domValue);
}, [domValue, targetElement, attribute]);

return null;
}
59 changes: 32 additions & 27 deletions src/components/Patient/EncounterQuestionnaire.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Card, CardContent } from "@/components/ui/card";
import Page from "@/components/Common/Page";
import { QuestionnaireForm } from "@/components/Questionnaire/QuestionnaireForm";

import { PLUGIN_Component } from "@/PluginEngine";

interface Props {
facilityId: string;
patientId: string;
Expand All @@ -21,32 +23,35 @@ export default function EncounterQuestionnaire({
subjectType,
}: Props) {
return (
<Page
title="Questionnaire"
backUrl={`/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}`}
>
<div className="container mx-auto p-4">
<Card>
<CardContent className="pt-6">
<QuestionnaireForm
facilityId={facilityId}
patientId={patientId}
subjectType={subjectType}
encounterId={encounterId}
questionnaireSlug={questionnaireSlug}
onSubmit={() => {
if (encounterId) {
navigate(
`/facility/${facilityId}/encounter/${encounterId}/updates`,
);
} else {
navigate(`/patient/${patientId}/updates`);
}
}}
/>
</CardContent>
</Card>
</div>
</Page>
<>
<PLUGIN_Component __name="Scribe" />
<Page
title="Questionnaire"
backUrl={`/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}`}
>
<div className="container mx-auto p-4">
<Card>
<CardContent className="pt-6">
<QuestionnaireForm
facilityId={facilityId}
patientId={patientId}
subjectType={subjectType}
encounterId={encounterId}
questionnaireSlug={questionnaireSlug}
onSubmit={() => {
if (encounterId) {
navigate(
`/facility/${facilityId}/encounter/${encounterId}/updates`,
);
} else {
navigate(`/patient/${patientId}/updates`);
}
}}
/>
</CardContent>
</Card>
</div>
</Page>
</>
);
}
50 changes: 48 additions & 2 deletions src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {

import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect";

import ScribeStructuredInput from "@/Utils/scribe";
import { AllergyIntolerance } from "@/types/emr/allergyIntolerance";
import { QuestionnaireResponse } from "@/types/questionnaire/form";

Expand Down Expand Up @@ -101,7 +102,52 @@ export function AllergyQuestion({
{question.text}
{question.required && <span className="ml-1 text-red-500">*</span>}
</Label>
<div className="rounded-lg border p-4">
<ScribeStructuredInput
value={allergies}
onChange={(value) => {
if (value) {
setAllergies(value);
updateQuestionnaireResponseCB({
...questionnaireResponse,
values: [
{
type: "allergy_intolerance",
value: value,
},
],
});
}
}}
name="Allergies"
prompt={`An array of objects of the following type: {
code: {
code: string,
display: string,
system: "http://snomed.info/sct"
},
clinical_status?: "active" | "inactive" | "resolved",
category?: "food" | "medication" | "environment" | "biologic",
criticality?: "low" | "high" | "unable-to-assess",
verification?: "unconfirmed" | "presumed" | "confirmed" | "refuted" | "entered-in-error"
last_occurrence?: YYYY-MM-DD string,
note?: string
}. Update existing data, delete existing data or append to the existing list as per the will of the user. Current date is ${new Date().toLocaleDateString()}`}
Comment on lines +144 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Whitelist or sanitize user-driven updates.
prompt suggests user instructions for editing structured data. Make sure your application defends against malicious or malformed data (e.g., unexpected property injection). Proper server-side validations are essential.

example={[
{
code: {
code: "842825221000119100",
display: "Anifrolumab",
system: "http://snomed.info/sct",
},
clinical_status: "inactive",
category: "environment",
criticality: "high",
last_occurrence: "2024-12-11",
note: "212",
},
]}
className="rounded-lg border p-4"
>
<div className="overflow-x-auto">
<Table>
<TableHeader>
Expand Down Expand Up @@ -261,7 +307,7 @@ export function AllergyQuestion({
<PlusIcon className="mr-2 h-4 w-4" />
Add Allergy
</Button>
</div>
</ScribeStructuredInput>
</div>
);
}
32 changes: 8 additions & 24 deletions src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { memo } from "react";

import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Select from "@/components/ui/select-util";

import { properCase } from "@/Utils/utils";
import type { QuestionnaireResponse } from "@/types/questionnaire/form";
import type { AnswerOption, Question } from "@/types/questionnaire/question";
import type { Question } from "@/types/questionnaire/question";

interface ChoiceQuestionProps {
question: Question;
Expand Down Expand Up @@ -57,23 +51,13 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({
</Label>
<Select
value={currentValue}
onValueChange={handleValueChange}
onChange={handleValueChange}
disabled={disabled}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
{options.map((option: AnswerOption) => (
<SelectItem
key={option.value.toString()}
value={option.value.toString()}
>
{properCase(option.display || option.value)}
</SelectItem>
))}
</SelectContent>
</Select>
options={options.map((option) => ({
label: properCase(option.display || option.value),
value: option.value.toString(),
}))}
/>
</div>
);
});
Loading
Loading