Skip to main content
Glama
BotEditor.tsx8.46 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, Grid, Group, JsonInput, NativeSelect, Paper } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import type { MedplumClient, PatchOperation } from '@medplum/core'; import { ContentType, isUUID, normalizeErrorString } from '@medplum/core'; import type { Bot } from '@medplum/fhirtypes'; import { sendCommand, useMedplum } from '@medplum/react'; import { IconCloudUpload, IconDeviceFloppy, IconPlayerPlay } from '@tabler/icons-react'; import type { JSX, SyntheticEvent } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router'; import classes from './BotEditor.module.css'; import { BotRunner } from './BotRunner'; import { CodeEditor } from './CodeEditor'; const DEFAULT_FHIR_INPUT = `{ "resourceType": "Patient", "name": [ { "given": [ "Alice" ], "family": "Smith" } ] }`; const DEFAULT_HL7_INPUT = 'MSH|^~\\&|ADT1|MCM|LABADT|MCM|198808181126|SECURITY|ADT|MSG00001|P|2.1\r' + 'PID|||PATID1234^5^M11||JONES^WILLIAM^A^III||19610615|M-||C|1200 N ELM STREET^^GREENSBORO^NC^27401-1020|GL|(919)379-1212|(919)271-3434||S||PATID12345001^2^M10|123456789|987654^NC\r' + 'NK1|1|JONES^BARBARA^K|SPO|||||20011105\r' + 'PV1|1|I|2000^2012^01||||004777^LEBAUER^SIDNEY^J.|||SUR||-||1|A0-'; export function BotEditor(): JSX.Element | null { const medplum = useMedplum(); const { id } = useParams() as { id: string }; const [bot, setBot] = useState<Bot>(); const [module, setModule] = useState<'commonjs' | 'esnext'>(); const [defaultCode, setDefaultCode] = useState<string>(); const [fhirInput, setFhirInput] = useState(DEFAULT_FHIR_INPUT); const [hl7Input, setHl7Input] = useState(DEFAULT_HL7_INPUT); const [contentType, setContentType] = useState<string>(ContentType.FHIR_JSON); const codeFrameRef = useRef<HTMLIFrameElement>(null); const outputFrameRef = useRef<HTMLIFrameElement>(null); const [loading, setLoading] = useState(false); useEffect(() => { medplum .readResource('Bot', id) .then(async (newBot: Bot) => { setBot(newBot); setModule(newBot.runtimeVersion === 'vmcontext' ? 'commonjs' : 'esnext'); setDefaultCode(await getBotCode(medplum, newBot)); }) .catch((err) => showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false })); }, [medplum, id]); // Gets the uncompiled TS code const getCode = useCallback(async () => { return sendCommand<undefined, string>(codeFrameRef.current as HTMLIFrameElement, { command: 'getValue' }); }, []); // Gets the compiled JS output const getCodeOutput = useCallback(async () => { return sendCommand<undefined, string>(codeFrameRef.current as HTMLIFrameElement, { command: 'getOutput' }); }, []); const getSampleInput = useCallback(async () => { if (contentType === ContentType.FHIR_JSON) { return JSON.parse(fhirInput); } else { return hl7Input; } }, [contentType, fhirInput, hl7Input]); const saveBot = useCallback( async (e: SyntheticEvent) => { e.preventDefault(); e.stopPropagation(); setLoading(true); try { const code = await getCode(); const codeOutput = await getCodeOutput(); const sourceCode = await medplum.createAttachment({ data: code, filename: 'index.ts', contentType: ContentType.TYPESCRIPT, }); const executableCode = await medplum.createAttachment({ data: codeOutput, filename: module === 'commonjs' ? 'index.cjs' : 'index.mjs', contentType: ContentType.JAVASCRIPT, }); const operations: PatchOperation[] = [ { op: 'add', path: '/sourceCode', value: sourceCode, }, { op: 'add', path: '/executableCode', value: executableCode, }, ]; await medplum.patchResource('Bot', id, operations); showNotification({ color: 'green', message: 'Saved' }); } catch (err) { showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false }); } finally { setLoading(false); } }, [medplum, id, module, getCode, getCodeOutput] ); const deployBot = useCallback( async (e: SyntheticEvent) => { e.preventDefault(); e.stopPropagation(); setLoading(true); try { await medplum.post(medplum.fhirUrl('Bot', id, '$deploy')); showNotification({ color: 'green', message: 'Deployed' }); } catch (err) { showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false }); } finally { setLoading(false); } }, [medplum, id] ); const executeBot = useCallback( async (e: SyntheticEvent) => { e.preventDefault(); e.stopPropagation(); setLoading(true); try { const input = await getSampleInput(); const result = await medplum.post(medplum.fhirUrl('Bot', id, '$execute'), input, contentType); await sendCommand(outputFrameRef.current as HTMLIFrameElement, { command: 'setValue', value: result, }); showNotification({ color: 'green', message: 'Success' }); } catch (err) { showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false }); } finally { setLoading(false); } }, [medplum, id, getSampleInput, contentType] ); if (!bot || defaultCode === undefined) { return null; } return ( <Grid m={0} gutter={0} style={{ overflow: 'hidden' }}> <Grid.Col span={8}> <Paper m={2} pb="xs" pr="xs" pt="xs" shadow="md" mih={400}> <CodeEditor iframeRef={codeFrameRef} language="typescript" module={module} testId="code-frame" defaultValue={defaultCode} minHeight="528px" /> <Group justify="flex-end" gap="xs"> <Button type="button" onClick={saveBot} loading={loading} leftSection={<IconDeviceFloppy size="1rem" />}> Save </Button> <Button type="button" onClick={deployBot} loading={loading} leftSection={<IconCloudUpload size="1rem" />}> Deploy </Button> <Button type="button" onClick={executeBot} loading={loading} leftSection={<IconPlayerPlay size="1rem" />}> Execute </Button> </Group> </Paper> </Grid.Col> <Grid.Col span={4}> <Paper m={2} pb="xs" pr="xs" pt="xs" shadow="md"> <NativeSelect data={[ { label: 'FHIR', value: ContentType.FHIR_JSON }, { label: 'HL7', value: ContentType.HL7_V2 }, ]} onChange={(e) => setContentType(e.currentTarget.value)} /> {contentType === ContentType.FHIR_JSON ? ( <JsonInput value={fhirInput} onChange={(newValue) => setFhirInput(newValue)} autosize minRows={15} /> ) : ( <textarea className={classes.hl7Input} value={hl7Input} onChange={(e) => setHl7Input(e.currentTarget.value)} rows={15} /> )} </Paper> <Paper m={2} p="xs" shadow="md"> <BotRunner iframeRef={outputFrameRef} className="medplum-bot-output-frame" testId="output-frame" minHeight="200px" /> </Paper> </Grid.Col> </Grid> ); } async function getBotCode(medplum: MedplumClient, bot: Bot): Promise<string> { if (bot.sourceCode?.url) { // Medplum storage service does not allow CORS requests for security reasons. // So instead, we have to use the FHIR Binary API to fetch the source code. // Example: https://storage.staging.medplum.com/binary/272a11dc-5b01-4c05-a14e-5bf53117e1e9/69303e8d-36f2-4417-b09b-60c15f221b09?Expires=... // The Binary ID is the first UUID in the URL. const binaryId = bot.sourceCode.url?.split('/')?.find(isUUID) as string; const blob = await medplum.download(medplum.fhirUrl('Binary', binaryId)); return blob.text(); } return bot.code ?? ''; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server