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 17 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
4 changes: 2 additions & 2 deletions .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
REACT_KERALA_GEO_ID=083d33fe-0c4b-4206-92ea-75d72e8e934c
9 changes: 0 additions & 9 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 Down Expand Up @@ -1645,7 +1641,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 @@ -1694,9 +1689,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 +1836,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 Down
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;
}
55 changes: 30 additions & 25 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,30 +23,33 @@ export default function EncounterQuestionnaire({
subjectType,
}: Props) {
return (
<Page
title="Questionnaire"
backUrl={`/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}`}
>
<Card className="mt-2">
<CardContent className="lg:p-4 p-0">
<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>
</Page>
<>
<PLUGIN_Component __name="Scribe" />
<Page
title="Questionnaire"
backUrl={`/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}`}
>
<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>
</Page>
</>
);
}
139 changes: 94 additions & 45 deletions src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {

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

import ScribeStructuredInput from "@/Utils/scribe";
import { AllergyIntolerance } from "@/types/emr/allergyIntolerance";
import { Code } from "@/types/questionnaire/code";
import { QuestionnaireResponse } from "@/types/questionnaire/form";
Expand Down Expand Up @@ -124,52 +125,100 @@ export function AllergyQuestion({
{question.text}
{question.required && <span className="ml-1 text-red-500">*</span>}
</Label>
{allergies.length > 0 && (
<div className="rounded-lg border">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[40px]"></TableHead>
<TableHead className="w-[220px]">Substance</TableHead>
<TableHead className="w-[85px] text-center whitespace-normal px-0.5">
Clinical
<br />
Status
</TableHead>
<TableHead className="w-[65px] text-center px-0.5">
Critical
</TableHead>
<TableHead className="w-[85px] text-center px-0.5">
Status
</TableHead>
<TableHead className="w-[125px] text-center px-0.5">
Occurrence
</TableHead>
<TableHead className="w-[35px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allergies.map((allergy, index) => (
<AllergyTableRow
key={index}
allergy={allergy}
disabled={disabled}
onUpdate={(updates) => handleUpdateAllergy(index, updates)}
onRemove={() => handleRemoveAllergy(index)}
/>
))}
</TableBody>
</Table>
<ScribeStructuredInput
value={allergies}
onChange={(value) => {
if (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"
>
{allergies.length > 0 && (
<div className="rounded-lg border">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[40px]"></TableHead>
<TableHead className="w-[220px]">Substance</TableHead>
<TableHead className="w-[85px] text-center whitespace-normal px-0.5">
Clinical
<br />
Status
</TableHead>
<TableHead className="w-[65px] text-center px-0.5">
Critical
</TableHead>
<TableHead className="w-[85px] text-center px-0.5">
Status
</TableHead>
<TableHead className="w-[125px] text-center px-0.5">
Occurrence
</TableHead>
<TableHead className="w-[35px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allergies.map((allergy, index) => (
<AllergyTableRow
key={index}
allergy={allergy}
disabled={disabled}
onUpdate={(updates) =>
handleUpdateAllergy(index, updates)
}
onRemove={() => handleRemoveAllergy(index)}
/>
))}
</TableBody>
</Table>
</div>
</div>
</div>
)}
<ValueSetSelect
system="system-allergy-code"
placeholder="Search for allergies to add"
onSelect={handleAddAllergy}
disabled={disabled}
/>
)}
<ValueSetSelect
system="system-allergy-code"
placeholder="Search for allergies to add"
onSelect={handleAddAllergy}
disabled={disabled}
/>
</ScribeStructuredInput>
</div>
);
}
Expand Down
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