Compare commits

...

3 Commits

Author SHA1 Message Date
Shivank Kacker
a0820d714a cleanups 2025-02-13 16:34:39 +05:30
Shivank Kacker
28bd137269 add ocr support 2025-02-11 23:37:12 +05:30
Shivank Kacker
61e95c6842 Remove redundant info from schema and optimize tokens 2025-02-05 21:35:14 +05:30
9 changed files with 522 additions and 246 deletions

95
package-lock.json generated
View File

@ -43,6 +43,8 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.13.0",
"postcss": "^8.4.49",
"prettier": "3.5.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.1",
@ -3458,6 +3460,99 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.6.11",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
"integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
"dev": true,
"engines": {
"node": ">=14.21.3"
},
"peerDependencies": {
"@ianvs/prettier-plugin-sort-imports": "*",
"@prettier/plugin-pug": "*",
"@shopify/prettier-plugin-liquid": "*",
"@trivago/prettier-plugin-sort-imports": "*",
"@zackad/prettier-plugin-twig": "*",
"prettier": "^3.0",
"prettier-plugin-astro": "*",
"prettier-plugin-css-order": "*",
"prettier-plugin-import-sort": "*",
"prettier-plugin-jsdoc": "*",
"prettier-plugin-marko": "*",
"prettier-plugin-multiline-arrays": "*",
"prettier-plugin-organize-attributes": "*",
"prettier-plugin-organize-imports": "*",
"prettier-plugin-sort-imports": "*",
"prettier-plugin-style-order": "*",
"prettier-plugin-svelte": "*"
},
"peerDependenciesMeta": {
"@ianvs/prettier-plugin-sort-imports": {
"optional": true
},
"@prettier/plugin-pug": {
"optional": true
},
"@shopify/prettier-plugin-liquid": {
"optional": true
},
"@trivago/prettier-plugin-sort-imports": {
"optional": true
},
"@zackad/prettier-plugin-twig": {
"optional": true
},
"prettier-plugin-astro": {
"optional": true
},
"prettier-plugin-css-order": {
"optional": true
},
"prettier-plugin-import-sort": {
"optional": true
},
"prettier-plugin-jsdoc": {
"optional": true
},
"prettier-plugin-marko": {
"optional": true
},
"prettier-plugin-multiline-arrays": {
"optional": true
},
"prettier-plugin-organize-attributes": {
"optional": true
},
"prettier-plugin-organize-imports": {
"optional": true
},
"prettier-plugin-sort-imports": {
"optional": true
},
"prettier-plugin-style-order": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
}
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -4,8 +4,7 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite preview",
"watch": "vite preview & vite build --watch",
"dev": "vite preview & vite build --watch",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
@ -34,6 +33,8 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.13.0",
"postcss": "^8.4.49",
"prettier": "3.5.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.1",

View File

@ -2,6 +2,7 @@ import { useState } from "react";
import {
ScribeField,
ScribeFieldSuggestion,
ScribeFileType,
ScribeStatus,
VALUESET_SYSTEM_NAMES,
} from "../types";
@ -31,9 +32,11 @@ import {
ChevronUpIcon,
Cross1Icon,
CrossCircledIcon,
ImageIcon,
} from "@radix-ui/react-icons";
import { useScribePosition } from "@/utils/controller-position";
import { printNode, zodToTs } from "zod-to-ts";
import FileUpload from "./FileUpload";
export function Controller(props: {
formState: unknown;
@ -48,6 +51,7 @@ export function Controller(props: {
const [toReview, setToReview] = useState<ScribeFieldSuggestion[]>();
const [openEditTranscript, setOpenEditTranscript] = useState(false);
const [controllerPosition] = useScribePosition();
const [files, setFiles] = useState<File[]>([]);
//Use this to test scribe
const SCRIBE_TEST_INPUT = `The patient's encounter status is currently on hold, classified as an emergency with a priority of “as needed,”
@ -80,7 +84,7 @@ export function Controller(props: {
const res = await API.scribe.get(scribeInstanceId);
const { status, transcript, ai_response } = res;
if (status === "FAILED") {
if (status === "FAILED" || status === "REFUSED") {
toast({ title: "Transcription failed", variant: "destructive" });
clearInterval(interval);
return reject(new Error("Transcription failed"));
@ -125,6 +129,10 @@ export function Controller(props: {
"ai_response",
);
const parsedFormData = JSON.parse(updatedFieldsResponse ?? "{}");
const scribeTranscription = parsedFormData.__scribe__transcription;
if (scribeTranscription && files.length !== 0) {
setTranscript(scribeTranscription);
}
// run type validations
const changedData = Object.entries(parsedFormData)
.map(([k, v]) => {
@ -229,24 +237,29 @@ export function Controller(props: {
};
// Uploads a scribe audio blob. Returns the response of the upload.
const uploadAudio = async (audioBlob: Blob, scribeInstanceId: string) => {
const category = "AUDIO";
const name = "audio.mp3";
const uploadScribeFile = async (
blob: Blob,
scribeInstanceId: string,
type: ScribeFileType,
) => {
const category = type === ScribeFileType.AUDIO ? "AUDIO" : "UNSPECIFIED";
const extension = blob?.type?.split("/")?.[1].split(";")?.[0];
const name = "file" + (extension ? `.${extension}` : "");
const filename = Date.now().toString();
const data = await API.scribe.createFileUpload({
original_name: name,
file_type: 1,
file_type: type,
name: filename,
associating_id: scribeInstanceId,
file_category: category,
mime_type: audioBlob?.type?.split(";")?.[0],
mime_type: blob?.type?.split(";")?.[0],
});
await new Promise<void>((resolve, reject) => {
const url = data?.signed_url;
const internal_name = data?.internal_name;
const f = audioBlob;
const f = blob;
if (f === undefined) {
reject(Error("No file to upload"));
return;
@ -270,7 +283,7 @@ export function Controller(props: {
return await API.scribe.editFileUpload(
data.id,
"SCRIBE",
type === ScribeFileType.AUDIO ? "SCRIBE_AUDIO" : "SCRIBE_DOCUMENT",
scribeInstanceId,
{ upload_completed: true },
);
@ -282,13 +295,21 @@ export function Controller(props: {
const data = await API.scribe.create({
status: "CREATED",
form_data: hfields as any,
// system_prompt: "...",
// json_prompt: "...",
// prompt: "..."
});
await Promise.all(
audioBlobs.map((blob) => uploadAudio(blob, data?.external_id ?? "")),
);
await Promise.all([
...audioBlobs.map((blob) =>
uploadScribeFile(blob, data?.external_id ?? "", ScribeFileType.AUDIO),
),
...files.map((file) =>
uploadScribeFile(
file,
data?.external_id ?? "",
ScribeFileType.DOCUMENT,
),
),
]);
return data.external_id;
};
@ -394,7 +415,25 @@ export function Controller(props: {
const handleCancel = () => {
setStatus("IDLE");
resetRecording();
setToReview(undefined);
setFiles([]);
setTranscript(undefined);
setLastTranscript(undefined);
};
const handleProcessFile = async () => {
setStatus("UPLOADING");
const fields = getQuestionInputs(props.formState);
const instanceId = await createScribeInstance(fields);
setInstanceId(instanceId);
setStatus("TRANSCRIBING");
await getTranscript(instanceId);
setStatus("THINKING");
const aiResponse = await getAIResponse(instanceId, fields);
if (!aiResponse) return;
setStatus("REVIEWING");
setToReview(getFieldsToReview(aiResponse, fields));
};
return (
@ -407,6 +446,9 @@ export function Controller(props: {
<div
className={`${status === "IDLE" ? "max-h-0 opacity-0" : "max-h-[400px]"} w-full overflow-hidden rounded-2xl ${status === "REVIEWING" && !(openEditTranscript || (toReview && !toReview.length)) ? "" : "border-secondary-400 border"} bg-white transition-all delay-100`}
>
{status === "ATTACHING" && (
<FileUpload files={files} setFiles={setFiles} error={null} />
)}
{status === "RECORDING" && (
<div className="flex items-center justify-center p-4 py-10">
<div className="text-center">
@ -501,7 +543,7 @@ export function Controller(props: {
)}
{status === "FAILED" && (
<div className="flex flex-col items-center justify-center gap-4 px-4 py-10 text-red-500">
<CrossCircledIcon className="text-4xl" />
<CrossCircledIcon className="h-8 w-8" />
{t("scribe_error")}
</div>
)}
@ -518,7 +560,7 @@ export function Controller(props: {
</button>
)}
<div className="flex items-center gap-2">
{status === "REVIEWING" && (
{(status === "REVIEWING" || status === "ATTACHING") && (
<button
onClick={handleCancel}
className="border-secondary-400 bg-secondary-300 hover:bg-secondary-400 flex aspect-square h-full items-center justify-center rounded-full border p-4 text-xl transition-all"
@ -527,13 +569,27 @@ export function Controller(props: {
<Cross1Icon />
</button>
)}
{status === "IDLE" && (
<button
onClick={() => setStatus("ATTACHING")}
className="border-secondary-400 bg-secondary-300 hover:bg-secondary-400 flex aspect-square h-full items-center justify-center rounded-full border p-4 text-xl transition-all"
>
<ImageIcon />
</button>
)}
<ScribeButton
files={files}
status={status}
onClick={
status !== "RECORDING"
? handleStartRecording
: handleStopRecording
status === "ATTACHING"
? handleProcessFile
: files.length > 0
? () => setStatus("ATTACHING")
: status !== "RECORDING"
? handleStartRecording
: handleStopRecording
}
disabled={status === "ATTACHING" && files.length === 0}
/>
</div>
</div>
@ -549,6 +605,7 @@ export function Controller(props: {
});
setToReview(undefined);
setStatus("IDLE");
setFiles([]);
}}
/>
)}

View File

@ -0,0 +1,100 @@
import { Cross2Icon, UploadIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export default function FileUpload(props: {
files: File[];
setFiles: (files: File[]) => void;
error: string | null;
}) {
const supported = ["image/jpeg", "image/png", "image/jpg"];
const { t } = useTranslation();
const { setFiles, error, files } = props;
const [isDragging, setIsDragging] = useState(false);
const handleDragEnter = (e: React.DragEvent<HTMLLabelElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragOver = (e: React.DragEvent<HTMLLabelElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent<HTMLLabelElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const droppedFiles = Array.from(e.dataTransfer.files).filter((file) =>
supported.includes(file.type),
);
if (droppedFiles.length > 0) {
setFiles([...files, ...droppedFiles]);
}
e.dataTransfer.clearData();
}
};
return (
<div className="flex flex-col items-center justify-center gap-4 p-4">
<div className="font-bold">{t("upload_images")}</div>
{!!files.length && (
<div className="flex max-w-[300px] flex-wrap items-center gap-2">
{files.map((file, index) => (
<div className="relative rounded-md shadow-md" key={index}>
<button
onClick={() => {
setFiles(files.filter((_, i) => i !== index));
}}
className="absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-white shadow-md"
>
<Cross2Icon className="h-4 w-4" />
</button>
<img
src={URL.createObjectURL(file)}
alt="uploaded"
className="h-10 w-10 rounded-md object-cover"
/>
</div>
))}
</div>
)}
<label
className={`cursor-pointer border border-dashed ${
isDragging ? "border-blue-500 bg-blue-100" : "border-gray-300"
} flex w-full flex-col items-center justify-center gap-4 rounded-md p-4 transition-all hover:bg-gray-100`}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<UploadIcon className="h-10 w-10" />
<div className="text-center text-xs text-gray-500">
{t("upload_images_description")}
</div>
<input
accept={supported.join(",")}
className="hidden"
type="file"
multiple
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
setFiles([...files, ...e.target.files]);
}
}}
/>
</label>
{error && <p>{error}</p>}
</div>
);
}

View File

@ -1,4 +1,4 @@
import { ReloadIcon } from "@radix-ui/react-icons";
import { ImageIcon, ReloadIcon } from "@radix-ui/react-icons";
import { ScribeStatus } from "../types";
import { useTranslation } from "react-i18next";
import useKeyboardShortcut from "use-keyboard-shortcut";
@ -10,10 +10,12 @@ import {
import { useRef, useState } from "react";
export default function ScribeButton(props: {
files: File[];
status: ScribeStatus;
onClick: () => void;
disabled?: boolean;
}) {
const { status, onClick } = props;
const { status, onClick, disabled, files } = props;
const { t } = useTranslation();
const [, setControllerPosition] = useScribePosition();
const [initMousePosition, setInitMousePosition] = useState<{
@ -105,7 +107,7 @@ export default function ScribeButton(props: {
onMouseLeave={handleDragEnd}
onClick={() => (!estimatedMovingPosition ? onClick() : undefined)}
className={`group z-10 flex items-center rounded-full ${status === "IDLE" ? "bg-primary-500 hover:bg-primary-600 text-white" : "border-secondary-400 bg-secondary-200 hover:bg-secondary-300 border"} ${!!estimatedMovingPosition ? "opacity-50" : ""} disabled:bg-secondary-300 transition-[background,top,right,left,bottom,opacity]`}
disabled={["TRANSCRIBING", "THINKING"].includes(status)}
disabled={["TRANSCRIBING", "THINKING"].includes(status) || disabled}
style={{ touchAction: "none" }}
>
<div
@ -115,6 +117,8 @@ export default function ScribeButton(props: {
<MicrophoneIcon className="w-4 invert" />
) : status === "RECORDING" ? (
<MicrophoneSlashIcon className="w-5" />
) : status === "ATTACHING" ? (
<ImageIcon />
) : (
<ReloadIcon />
)}
@ -122,9 +126,13 @@ export default function ScribeButton(props: {
<div className="pl-2 pr-6 font-semibold">
{status === "IDLE"
? t("voice_autofill")
: status === "RECORDING"
? t("stop_recording")
: t("retake_recording")}
: status === "ATTACHING"
? t("process_images")
: status === "RECORDING"
? t("stop_recording")
: files.length > 0
? t("reupload_files")
: t("retake_recording")}
</div>
</button>
</>

View File

@ -1,20 +1,25 @@
{
"voice_autofill": "Voice Autofill",
"hearing": "We are hearing you...",
"copilot_thinking": "Copilot is thinking...",
"could_not_autofill": "We could not autofill any fields from what you said",
"transcribe_again": "Transcribe Again",
"transcript_edit_info": "You can update this if we made an error",
"transcript_information": "This is what we heard",
"process_transcript": "Process Again",
"retake_recording": "Retake Recording",
"scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}",
"scribe_error": "Could not autofill fields",
"accept": "Accept",
"accept_all": "Accept All",
"reject": "Reject",
"stop_recording": "Stop Recording",
"autofilled_fields": "Autofilled Fields",
"start_review": "Start Review",
"scribe_no_match": "Copilot could not find a {{valueType}} that matches with \"{{query}}\". Please enter manually."
}
"voice_autofill": "Voice Autofill",
"hearing": "We are hearing you...",
"copilot_thinking": "Copilot is thinking...",
"could_not_autofill": "We could not autofill any fields",
"transcribe_again": "Transcribe Again",
"transcript_edit_info": "You can update this if we made an error",
"transcript_information": "This is what we understood",
"process_transcript": "Process Again",
"retake_recording": "Retake Recording",
"scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}",
"scribe_error": "Could not autofill fields",
"accept": "Accept",
"accept_all": "Accept All",
"reject": "Reject",
"stop_recording": "Stop Recording",
"autofilled_fields": "Autofilled Fields",
"start_review": "Start Review",
"scribe_no_match": "Copilot could not find a {{valueType}} that matches with \"{{query}}\". Please enter manually.",
"process_images": "Process Images",
"upload_images": "Upload Images",
"upload_images_description": "Drag and drop or click to upload images",
"cancel": "Cancel",
"reupload_files": "Reupload Files"
}

View File

@ -1,223 +1,233 @@
export type FeatureFlag = "SCRIBE_ENABLED"; // "HCX_ENABLED" | "ABDM_ENABLED" |
export type UserBareMinimum = {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
user_type: unknown;
last_login: string | undefined;
read_profile_picture_url?: string;
external_id: string;
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
user_type: unknown;
last_login: string | undefined;
read_profile_picture_url?: string;
external_id: string;
};
export type GenderType = "Male" | "Female" | "Transgender";
export type UserModel = UserBareMinimum & {
external_id: string;
local_body?: number;
district?: number;
state?: number;
video_connect_link: string;
phone_number?: string;
alt_phone_number?: string;
gender?: GenderType;
read_profile_picture_url?: string;
date_of_birth: Date | null | string;
is_superuser?: boolean;
verified?: boolean;
home_facility?: string;
qualification?: string;
doctor_experience_commenced_on?: string;
doctor_medical_council_registration?: string;
weekly_working_hours?: string | null;
user_flags?: FeatureFlag[];
external_id: string;
local_body?: number;
district?: number;
state?: number;
video_connect_link: string;
phone_number?: string;
alt_phone_number?: string;
gender?: GenderType;
read_profile_picture_url?: string;
date_of_birth: Date | null | string;
is_superuser?: boolean;
verified?: boolean;
home_facility?: string;
qualification?: string;
doctor_experience_commenced_on?: string;
doctor_medical_council_registration?: string;
weekly_working_hours?: string | null;
user_flags?: FeatureFlag[];
};
export type ScribeModel = {
external_id: string;
requested_by: UserModel;
form_data: {
friendlyName: string;
default: string;
description: string;
example: string;
id: string;
options?: any[];
type: string;
}[];
transcript: string;
ai_response: string;
status:
external_id: string;
requested_by: UserModel;
form_data: {
friendlyName: string;
default: string;
description: string;
example: string;
id: string;
options?: any[];
type: string;
}[];
transcript: string;
ai_response: string;
status:
| "CREATED"
| "READY"
| "GENERATING_TRANSCRIPT"
| "GENERATING_AI_RESPONSE"
| "COMPLETED"
| "REFUSED"
| "FAILED";
system_prompt?: string;
json_prompt?: string;
prompt?: string;
};
export type ScribeStatus =
| "FAILED"
| "IDLE"
| "RECORDING"
| "UPLOADING"
| "TRANSCRIBING"
| "THINKING"
| "REVIEWING"
| "SCRIBING";
| "FAILED"
| "IDLE"
| "ATTACHING"
| "RECORDING"
| "UPLOADING"
| "TRANSCRIBING"
| "THINKING"
| "REVIEWING"
| "SCRIBING";
export enum ScribeFileType {
OTHER = 0,
AUDIO = 1,
DOCUMENT = 2,
}
export type ScribeFieldOption = {
value: string,
text: string
}
value: string;
text: string;
};
export type ScribeField = {
question: FormQuestion,
fieldElement: Element,
value: string | null;
}
question: FormQuestion;
fieldElement: Element;
value: string | null;
};
export type ScribeAIResponse = {
[field_number: number]: unknown
}
[field_number: number]: unknown;
};
export type ScribePromptMap = {
[key in QuestionType | "default"]?: { prompt: string; example: unknown };
}
[key in QuestionType | "default"]?: { prompt: string; example: unknown };
};
export type ScribeFieldSuggestion = ScribeField & { newValue: unknown }
export type ScribeFieldSuggestion = ScribeField & { newValue: unknown };
export type ScribeFieldReviewedSuggestion = ScribeFieldSuggestion & { suggestionIndex: number, approved?: boolean }
export type ScribeFieldReviewedSuggestion = ScribeFieldSuggestion & {
suggestionIndex: number;
approved?: boolean;
};
export type FileCategory = "UNSPECIFIED" | "XRAY" | "AUDIO" | "IDENTITY_PROOF";
export interface CreateFileRequest {
file_type: string | number;
file_category: FileCategory;
name: string;
associating_id: string;
original_name: string;
mime_type: string;
file_type: ScribeFileType;
file_category: FileCategory;
name: string;
associating_id: string;
original_name: string;
mime_type: string;
}
export interface CreateFileResponse {
id: string;
file_type: string;
file_category: FileCategory;
signed_url: string;
internal_name: string;
id: string;
file_type: ScribeFileType;
file_category: FileCategory;
signed_url: string;
internal_name: string;
}
export interface FileUploadModel {
id?: string;
name?: string;
associating_id?: string;
created_date?: string;
upload_completed?: boolean;
uploaded_by?: UserBareMinimum;
file_category?: FileCategory;
read_signed_url?: string;
is_archived?: boolean;
archive_reason?: string;
extension?: string;
archived_by?: UserBareMinimum;
archived_datetime?: string;
id?: string;
name?: string;
associating_id?: string;
created_date?: string;
upload_completed?: boolean;
uploaded_by?: UserBareMinimum;
file_category?: FileCategory;
read_signed_url?: string;
is_archived?: boolean;
archive_reason?: string;
extension?: string;
archived_by?: UserBareMinimum;
archived_datetime?: string;
}
export interface FacilityModel {
id?: string;
name?: string;
read_cover_image_url?: string;
facility_type?: string;
address?: string;
features?: number[];
location?: {
latitude: number;
longitude: number;
};
phone_number?: string;
middleware_address?: string;
modified_date?: string;
created_date?: string;
state?: number;
district?: number;
local_body?: number;
ward?: number;
pincode?: string;
facility_flags?: FeatureFlag[];
latitude?: string;
longitude?: string;
kasp_empanelled?: boolean;
patient_count?: number;
bed_count?: number;
id?: string;
name?: string;
read_cover_image_url?: string;
facility_type?: string;
address?: string;
features?: number[];
location?: {
latitude: number;
longitude: number;
};
phone_number?: string;
middleware_address?: string;
modified_date?: string;
created_date?: string;
state?: number;
district?: number;
local_body?: number;
ward?: number;
pincode?: string;
facility_flags?: FeatureFlag[];
latitude?: string;
longitude?: string;
kasp_empanelled?: boolean;
patient_count?: number;
bed_count?: number;
}
export type QuestionType =
| "group"
| "display"
| "boolean"
| "decimal"
| "integer"
| "date"
| "dateTime"
| "time"
| "string"
| "text"
| "url"
| "choice"
| "quantity"
| "structured";
| "group"
| "display"
| "boolean"
| "decimal"
| "integer"
| "date"
| "dateTime"
| "time"
| "string"
| "text"
| "url"
| "choice"
| "quantity"
| "structured";
export interface FormQuestion {
id: string;
structured_type?: string;
answer_option?: { value: string }[];
text: string;
required?: boolean;
type: QuestionType
repeats?: boolean;
id: string;
structured_type?: string;
answer_option?: { value: string }[];
text: string;
required?: boolean;
type: QuestionType;
repeats?: boolean;
}
export type ValueSetSystem =
| "system-allergy-code"
| "system-condition-code"
| "system-medication"
| "system-additional-instruction"
| "system-administration-method"
| "system-as-needed-reason"
| "system-body-site"
| "system-route"
| "system-observation"
| "system-body-site-observation"
| "system-collection-method"
| "system-ucum-units";
| "system-allergy-code"
| "system-condition-code"
| "system-medication"
| "system-additional-instruction"
| "system-administration-method"
| "system-as-needed-reason"
| "system-body-site"
| "system-route"
| "system-observation"
| "system-body-site-observation"
| "system-collection-method"
| "system-ucum-units";
export const VALUESET_SYSTEM_NAMES: { [key in ValueSetSystem]: string } = {
"system-allergy-code": "Allergy",
"system-condition-code": "Condition",
"system-medication": "Medication",
"system-additional-instruction": "Additional Instruction",
"system-administration-method": "Administration Method",
"system-as-needed-reason": "As Needed Reason",
"system-body-site": "Body Site",
"system-route": "Route",
"system-observation": "Observation",
"system-body-site-observation": "Body Site Observation",
"system-collection-method": "Collection Method",
"system-ucum-units": "UCUM Units",
"system-allergy-code": "Allergy",
"system-condition-code": "Condition",
"system-medication": "Medication",
"system-additional-instruction": "Additional Instruction",
"system-administration-method": "Administration Method",
"system-as-needed-reason": "As Needed Reason",
"system-body-site": "Body Site",
"system-route": "Route",
"system-observation": "Observation",
"system-body-site-observation": "Body Site Observation",
"system-collection-method": "Collection Method",
"system-ucum-units": "UCUM Units",
};
export interface CodeSearchQuery {
code_search_type: ValueSetSystem,
code_search_query: string,
primary?: boolean;
code_search_type: ValueSetSystem;
code_search_query: string;
primary?: boolean;
}
export interface Code {
system: string;
code: string;
display?: string;
}
system: string;
code: string;
display?: string;
}

View File

@ -91,12 +91,12 @@ const withFallback = <T>(schema: z.ZodType<T>, fallback: T) =>
export const STRUCTURED_INPUT_PROMPTS = {
"encounter": {
prompt: () => z.object({
status: z.enum(["planned", "in_progress", "on_hold", "discharged", "completed", "cancelled", "discontinued", "entered_in_error", "unknown"]).describe("Status of the encounter"),
status: z.enum(["planned", "in_progress", "on_hold", "discharged", "completed", "cancelled", "discontinued", "entered_in_error", "unknown"]),
encounter_class: z.enum(["imp", "amb", "obsenc", "emer", "vr", "hh"]).describe(`Class of the encounter : "imp" (Inpatient (IP)) | "amb" (Ambulatory (OP)) | "obsenc" (Observation Room) | "emer" (Emergency) | "vr" (Virtual) | "hh" (Home Health)`),
priority: z.enum(ENCOUNTER_PRIORITY).describe("Priority of the encounter"),
priority: z.enum(ENCOUNTER_PRIORITY),
external_identifier: z.string().optional().describe("ip/op/obs/emr number"),
hospitalization: z.object({
re_admission: z.boolean().describe("Encounter is a readmission"),
re_admission: z.boolean(),
admit_source: z.enum(["hosp_trans"
, "emd"
, "outp"
@ -114,7 +114,7 @@ export const STRUCTURED_INPUT_PROMPTS = {
, "vegan"
, "halal"
, "kosher"
, "none"]).optional().describe("Dietary preference of the patient"),
, "none"]).optional(),
discharge_disposition: z.enum(["home"
, "alt_home"
, "other_hcf"
@ -144,13 +144,13 @@ export const STRUCTURED_INPUT_PROMPTS = {
},
"medication_request": {
prompt: (isRes?: boolean) => z.array(z.object({
status: z.enum(MEDICATION_REQUEST_STATUS).describe("Status of the medication"),
intent: z.enum(MEDICATION_REQUEST_INTENT).optional().describe("Intent of the medication request"),
category: z.enum(["inpatient", "outpatient", "community", "discharge"]).describe("Category of the medication request"),
priority: z.enum(["stat", "urgent", "asap", "routine"]).describe("Priority of the medication request"),
status: z.enum(MEDICATION_REQUEST_STATUS),
intent: z.enum(MEDICATION_REQUEST_INTENT).optional(),
category: z.enum(["inpatient", "outpatient", "community", "discharge"]),
priority: z.enum(["stat", "urgent", "asap", "routine"]),
do_not_perform: z.literal(false).describe("Do not update this value"),
medication: codeStructure(isRes, "system-medication", true),
authored_on: withFallback(isoDateTime.default(new Date().toISOString()).describe("When was this medication request authored? In ISO datetime"), new Date().toISOString()),
authored_on: withFallback(isoDateTime.default(new Date().toISOString()).describe("In ISO datetime"), new Date().toISOString()),
dosage_instruction: z.array(z.object({
sequence: z.number().optional(),
text: z.string().optional(),
@ -186,8 +186,8 @@ export const STRUCTURED_INPUT_PROMPTS = {
as_needed_boolean: withFallback(z.boolean().describe("True if the prescription is PRN, else false. Do not ommit this.").default(false), false),
as_needed_for: codeStructure(isRes, "system-as-needed-reason").optional().describe("If it is a PRN medication (as_needed_boolean is true), the indicator"),
site: codeStructure(isRes, "system-body-site").optional().describe("The site the medication should be administered at"),
route: codeStructure(isRes, "system-route").optional().describe("The route of the medicine"),
method: codeStructure(isRes, "system-administration-method").optional().describe("The method in which the medicine should be administered"),
route: codeStructure(isRes, "system-route").optional(),
method: codeStructure(isRes, "system-administration-method").optional(),
dose_and_rate: z.union([z.object({
type: z.literal("ordered"),
dose_quantity: doseQuantity,
@ -203,7 +203,7 @@ export const STRUCTURED_INPUT_PROMPTS = {
`),
max_dose_per_period: doseRange.optional()
})),
note: z.string().optional().describe("Additional Notes")
note: z.string().optional()
})),
example: [
{
@ -324,16 +324,16 @@ export const STRUCTURED_INPUT_PROMPTS = {
},
"medication_statement": {
prompt: (isRes?: boolean) => z.array(z.object({
status: z.enum(MEDICATION_STATEMENT_STATUS).describe("Status of the medication"),
dosage_text: z.string().optional().describe("Text to support the dosage"),
information_source: z.string().optional().describe("The information source of the medication"),
status: z.enum(MEDICATION_STATEMENT_STATUS),
dosage_text: z.string().optional(),
information_source: z.string().optional(),
medication: codeStructure(isRes, "system-medication", true),
note: z.string().optional().describe("Additional notes on the medication"),
reason: z.string().optional().describe("Reason for medication"),
note: z.string().optional(),
reason: z.string().optional(),
effective_period: z.object({
start: isoDateTime.describe("ISO date"),
end: isoDateTime.describe("ISO date")
}).optional().describe("Medication effective period")
}).optional()
})),
example: [
{
@ -357,12 +357,12 @@ export const STRUCTURED_INPUT_PROMPTS = {
"symptom": {
prompt: (isRes?: boolean) => z.array(z.object({
code: codeStructure(isRes, "system-condition-code", true),
clinical_status: z.enum(["active", "recurrence", "relapse", "inactive", "remission", "resolved"]).describe("Clinical Status of the symptom"),
verification_status: z.enum(["unconfirmed", "provisional", "differential", "confirmed", "refuted", "entered-in-error"]).describe("Verification status of the symptom"),
severity: z.enum(["severe", "moderate", "mild"]).optional().describe("Severity of the symptom"),
onset: withFallback(z.object({ onset_datetime: isoDateTime }).default({ onset_datetime: new Date().toISOString() }).describe("Onset date of the symptom in ISO format"), { onset_datetime: new Date().toISOString() }),
recorded_date: isoDateTime.optional().describe("Date the symptom was recorded in ISO format"),
note: z.string().optional().describe("Additional notes")
clinical_status: z.enum(["active", "recurrence", "relapse", "inactive", "remission", "resolved"]),
verification_status: z.enum(["unconfirmed", "provisional", "differential", "confirmed", "refuted", "entered-in-error"]),
severity: z.enum(["severe", "moderate", "mild"]),
onset: withFallback(z.object({ onset_datetime: isoDateTime }).default({ onset_datetime: new Date().toISOString() }).describe("ISO format"), { onset_datetime: new Date().toISOString() }),
recorded_date: isoDateTime.optional().describe("ISO format"),
note: z.string().optional()
})),
example: [
{
@ -384,11 +384,11 @@ export const STRUCTURED_INPUT_PROMPTS = {
"diagnosis": {
prompt: (isRes?: boolean) => z.array(z.object({
code: codeStructure(isRes, "system-condition-code", true),
clinical_status: z.enum(["active", "recurrence", "relapse", "inactive", "remission", "resolved"]).describe("Clincal Status of the diagnosis"),
verification_status: z.enum(["unconfirmed", "provisional", "differential", "confirmed", "refuted", "entered-in-error"]).describe("Verification Status of the diagnosis"),
onset: withFallback(z.object({ onset_datetime: isoDateTime }).default({ onset_datetime: new Date().toISOString() }).describe("Onset date of the symptom in ISO format"), { onset_datetime: new Date().toISOString() }),
recorded_date: isoDateTime.optional().describe("Date the diagnosis was recorded. In ISO format"),
note: z.string().optional().describe("Additional notes")
clinical_status: z.enum(["active", "recurrence", "relapse", "inactive", "remission", "resolved"]),
verification_status: z.enum(["unconfirmed", "provisional", "differential", "confirmed", "refuted", "entered-in-error"]),
onset: withFallback(z.object({ onset_datetime: isoDateTime }).default({ onset_datetime: new Date().toISOString() }).describe("ISO format"), { onset_datetime: new Date().toISOString() }),
recorded_date: withFallback(isoDateTime.describe("In ISO format").default(new Date().toISOString()), new Date().toISOString()),
note: z.string().optional()
})),
example: [
{
@ -409,12 +409,12 @@ export const STRUCTURED_INPUT_PROMPTS = {
"allergy_intolerance": {
prompt: (isRes?: boolean) => z.array(z.object({
code: codeStructure(isRes, "system-allergy-code", true),
clinical_status: z.enum(["active", "inactive", "resolved"]).optional().describe("Clincal status of the allergy"),
category: z.enum(["food", "medication", "environment", "biologic"]).optional().describe("Category of the allergy"),
criticality: z.enum(["low", "high", "unable-to-assess"]).optional().describe("How critical is the allergy"),
verification_status: z.enum(["unconfirmed", "presumed", "confirmed", "refuted", "entered-in-error"]).optional().describe("Verification Status of the allergy"),
last_occurence: isoDateTime.optional().describe("The last occurance of the allergy In ISO format"),
note: z.string().optional().describe("Additional Notes")
clinical_status: z.enum(["active", "inactive", "resolved"]).optional(),
category: z.enum(["food", "medication", "environment", "biologic"]).optional(),
criticality: z.enum(["low", "high", "unable-to-assess"]).optional(),
verification_status: z.enum(["unconfirmed", "presumed", "confirmed", "refuted", "entered-in-error"]).optional(),
last_occurence: isoDateTime.optional().describe("ISO format"),
note: z.string().optional()
})),
example: [
{
@ -433,7 +433,7 @@ export const STRUCTURED_INPUT_PROMPTS = {
},
"follow_up_appointment": {
prompt: () => z.object({
reason_for_visit: z.string().describe("The reason for the appointment")
reason_for_visit: z.string()
}),
example: {
reason_for_visit: "No change in condition"