diff --git a/.env.development b/.env.development index 4138fb8..e85b7ec 100644 --- a/.env.development +++ b/.env.development @@ -11,7 +11,7 @@ BATCH_SIZE=1 CHROMA_SERVICE_HOST=chromadb-dev CHROMA_SERVICE_PORT=8000 -LLM_SERVICE_HOST=gpu033.cluster.local +LLM_SERVICE_HOST=host.docker.internal LLM_SERVICE_PORT=8080 LLM_SERVICE_URL=http://${LLM_SERVICE_HOST}:${LLM_SERVICE_PORT}/v1 diff --git a/adrenaline/api/main.py b/adrenaline/api/main.py index 1ae7f59..63e3977 100644 --- a/adrenaline/api/main.py +++ b/adrenaline/api/main.py @@ -7,6 +7,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from api.patients.answer import initialize_llm from api.patients.db import check_database_connection from api.routes.answer import router as answer_router from api.routes.auth import router as auth_router @@ -49,7 +50,7 @@ async def startup_event() -> None: await init_db() async for session in get_async_session(): await create_initial_admin(session) - # await initialize_llm() + await initialize_llm() except Exception as e: logger.error(f"Startup failed: {str(e)}") raise diff --git a/adrenaline/api/patients/data.py b/adrenaline/api/patients/data.py index 079445d..703ea58 100644 --- a/adrenaline/api/patients/data.py +++ b/adrenaline/api/patients/data.py @@ -114,6 +114,12 @@ class Event(BaseModel): The numeric value of the event. text_value : Optional[str] The text value of the event. + event_type : Optional[str] + The type of the event. + environment : Optional[str] + The environment in which the event occurred. + details: Optional[str] + Additional details about the event. """ patient_id: int @@ -123,6 +129,9 @@ class Event(BaseModel): timestamp: Optional[datetime] numeric_value: Optional[float] text_value: Optional[str] + event_type: Optional[str] + environment: Optional[str] + details: Optional[str] class PatientData(BaseModel): diff --git a/adrenaline/api/patients/ehr.py b/adrenaline/api/patients/ehr.py index d564fbc..fe67c7e 100644 --- a/adrenaline/api/patients/ehr.py +++ b/adrenaline/api/patients/ehr.py @@ -9,55 +9,43 @@ from api.patients.data import Event -# Configure logging +# Configure logging with a consistent format +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO +) logger = logging.getLogger(__name__) class EHRDataManager: - """A class to manage EHR data. - - Attributes - ---------- - lazy_df : Optional[pl.LazyFrame] - The lazy DataFrame containing the EHR data. - """ + """A class to manage EHR data using Polars LazyFrames for optimal performance.""" def __init__(self) -> None: - """Initialize the EHRDataManager.""" self.lazy_df: Optional[pl.LazyFrame] = None + self._required_columns = { + "patient_id", + "encounter_id", + "code", + "description", + "timestamp", + "numeric_value", + "text_value", + } def init_lazy_df(self, directory: str) -> None: - """Initialize the lazy DataFrame. + """Initialize the LazyFrame with the given directory. Parameters ---------- directory : str - The directory containing the parquet files. + The directory containing the MEDS parquet files. """ if self.lazy_df is None: - parquet_files = [f for f in os.listdir(directory) if f.endswith(".parquet")] - if not parquet_files: - logger.error(f"No parquet files found in directory: {directory}") - raise ValueError(f"No parquet files found in directory: {directory}") - try: - self.lazy_df = pl.scan_parquet(os.path.join(directory, "*.parquet")) - # Verify that the required columns exist - existing_columns = self.lazy_df.collect_schema().names() - required_columns = [ - "subject_id", - "hadm_id", - "code", - "description", - "time", - "numeric_value", - "text_value", - ] - missing_columns = set(required_columns) - set(existing_columns) - if missing_columns: - raise ValueError(f"Missing required columns: {missing_columns}") + self.lazy_df = pl.scan_parquet( + os.path.join(directory, "*.parquet"), cache=True + ) - # Rename columns to match Event dataclass + # Ensure consistent column naming self.lazy_df = self.lazy_df.rename( { "subject_id": "patient_id", @@ -65,13 +53,47 @@ def init_lazy_df(self, directory: str) -> None: "time": "timestamp", } ) - logger.info("Lazy DataFrame initialized successfully") - except Exception as e: - logger.error(f"Error initializing lazy DataFrame: {str(e)}") + schema = set(self.lazy_df.collect_schema().names()) + missing_columns = self._required_columns - schema + if missing_columns: + raise ValueError(f"Missing required columns: {missing_columns}") + + logger.info("LazyFrame initialized successfully") + + except Exception: + logger.error("LazyFrame initialization failed", exc_info=True) raise + def _process_event(self, event_data: dict) -> dict: + """Process event data to extract event_type, environment, and details.""" + code_parts = event_data["code"].split("//") + + # Process event_type and environment + if len(code_parts) > 1 and code_parts[0] in ["HOSPITAL", "ICU"]: + environment = code_parts[0] + event_type = code_parts[1] + else: + environment = None + event_type = code_parts[0] + + # Process details based on event type + if event_type in ["MEDICATION", "GENDER"]: + if event_type == "GENDER": + details = code_parts[1] + else: + details = ", ".join(code_parts[2:]) + else: + details = event_data["description"] + + return { + **event_data, + "event_type": event_type, + "environment": environment, + "details": details, + } + def fetch_patient_events(self, patient_id: int) -> List[Event]: - """Fetch events for a patient. + """Fetch all events for a patient. Parameters ---------- @@ -81,145 +103,92 @@ def fetch_patient_events(self, patient_id: int) -> List[Event]: Returns ------- List[Event] - The events for the patient. + List of events for the patient. """ if self.lazy_df is None: - raise ValueError("Lazy DataFrame not initialized. Call init_lazy_df first.") + raise ValueError("LazyFrame not initialized") try: filtered_df = ( self.lazy_df.filter(pl.col("patient_id") == patient_id) - .select( - [ - "patient_id", - "encounter_id", - "code", - "description", - "timestamp", - "numeric_value", - "text_value", - ] - ) - .collect() + .select(list(self._required_columns)) + .collect(streaming=True) ) - return [ - Event( - patient_id=row["patient_id"], - encounter_id=row["encounter_id"], - code=row["code"], - description=row["description"], - timestamp=row["timestamp"], - numeric_value=row["numeric_value"], - text_value=row["text_value"], - ) - for row in filtered_df.to_dicts() + # Process each event + processed_events = [ + self._process_event(row) for row in filtered_df.to_dicts() ] - except Exception as e: - logger.error(f"Error fetching events for patient ID {patient_id}: {str(e)}") + return [Event(**event) for event in processed_events] + except Exception: + logger.error( + f"Error fetching events for patient {patient_id}", exc_info=True + ) raise def fetch_recent_encounter_events(self, patient_id: int) -> List[Event]: - """Fetch events from the most recent encounter for a patient. - - Parameters - ---------- - patient_id : int - The patient ID. - - Returns - ------- - List[Event] - The events from the most recent encounter for the patient. - """ + """Fetch events from most recent encounter with optimized query.""" if self.lazy_df is None: - raise ValueError("Lazy DataFrame not initialized. Call init_lazy_df first.") + raise ValueError("LazyFrame not initialized") try: - # First get the most recent encounter ID - most_recent_encounter = ( - self.lazy_df - .filter(pl.col("patient_id") == patient_id) - .select([ - "encounter_id", - "timestamp" - ]) + # Optimize query by combining operations + events_df = ( + self.lazy_df.filter(pl.col("patient_id") == patient_id) .sort("timestamp", descending=True) - .unique(subset="encounter_id") + .group_by("encounter_id") + .agg( + [ + pl.first("timestamp").alias("latest_timestamp"), + pl.all().exclude(["timestamp"]), + ] + ) .limit(1) - .collect() + .explode(list(self._required_columns - {"timestamp"})) + .collect(streaming=True) ) - if most_recent_encounter.height == 0: - logger.warning(f"No encounters found for patient ID {patient_id}") + if events_df.height == 0: + logger.info(f"No encounters found for patient {patient_id}") return [] - recent_encounter_id = most_recent_encounter.get_column("encounter_id")[0] + return [Event(**row) for row in events_df.to_dicts()] + except Exception: + logger.error( + f"Error fetching recent events for patient {patient_id}", exc_info=True + ) + raise - # Then fetch all events for this encounter + def fetch_patient_events_by_type( + self, patient_id: int, event_type: str + ) -> List[Event]: + """Fetch events filtered by event_type for a patient.""" + if self.lazy_df is None: + raise ValueError("LazyFrame not initialized") + + try: filtered_df = ( - self.lazy_df - .filter( - (pl.col("patient_id") == patient_id) & - (pl.col("encounter_id") == recent_encounter_id) - ) - .select([ - "patient_id", - "encounter_id", - "code", - "description", - "timestamp", - "numeric_value", - "text_value" - ]) - .sort("timestamp") - .collect() + self.lazy_df.filter(pl.col("patient_id") == patient_id) + .select(list(self._required_columns)) + .collect(streaming=True) ) - events = [ - Event( - patient_id=row["patient_id"], - encounter_id=row["encounter_id"], - code=row["code"], - description=row["description"], - timestamp=row["timestamp"], - numeric_value=row["numeric_value"], - text_value=row["text_value"], - ) + # Process each event and filter by event_type + processed_events = [ + self._process_event(row) for row in filtered_df.to_dicts() + if self._process_event(row)["event_type"] == event_type ] - - logger.info( - f"Retrieved {len(events)} events from most recent encounter " - f"{recent_encounter_id} for patient {patient_id}" - ) - return events - - except Exception as e: + return [Event(**event) for event in processed_events] + except Exception: logger.error( - f"Error fetching recent encounter events for patient ID {patient_id}: {str(e)}" + f"Error fetching events for patient {patient_id}", exc_info=True ) raise -# Create a single instance of EHRDataManager -ehr_data_manager: EHRDataManager = EHRDataManager() - - -# Use these functions to interact with the EHRDataManager -def init_lazy_df(directory: str) -> None: - """Initialize the lazy DataFrame. - - Parameters - ---------- - directory : str - The directory containing the parquet files. - """ - ehr_data_manager.init_lazy_df(directory) - - -def fetch_recent_encounter_events(patient_id: int) -> List[Event]: - """Fetch events from the most recent encounter for a patient. +def fetch_patient_encounters(patient_id: int) -> List[dict]: + """Fetch encounters with admission dates for a patient. Parameters ---------- @@ -228,57 +197,83 @@ def fetch_recent_encounter_events(patient_id: int) -> List[Event]: Returns ------- - List[Event] - The events from the most recent encounter for the patient. + List[dict] + List of encounters with their admission dates. + Format: [{"encounter_id": str, "admission_date": str}] """ - return ehr_data_manager.fetch_recent_encounter_events(patient_id) + if ehr_data_manager.lazy_df is None: + raise ValueError("Lazy DataFrame not initialized. Call init_lazy_df first.") + try: + # First get all events for the patient + filtered_df = ( + ehr_data_manager.lazy_df.filter(pl.col("patient_id") == patient_id) + .select(list(ehr_data_manager._required_columns)) + .collect(streaming=True) + ) -def fetch_patient_events(patient_id: int) -> List[Event]: - """Fetch events for a patient. + if filtered_df.height == 0: + logger.info(f"No events found for patient ID {patient_id}") + return [] + + # Process events to identify hospital admissions + encounters = {} + for row in filtered_df.to_dicts(): + processed_event = ehr_data_manager._process_event(row) + # Check if this is a hospital admission event + if processed_event["event_type"] == "HOSPITAL_ADMISSION": + encounter_id = str(processed_event["encounter_id"]) + timestamp = processed_event["timestamp"] + + # Store only the earliest admission date for each encounter + if ( + encounter_id not in encounters + or timestamp < encounters[encounter_id] + ): + encounters[encounter_id] = timestamp + + # Convert to list of dictionaries and sort by date + encounter_list = [ + { + "encounter_id": encounter_id, + "admission_date": timestamp.strftime("%Y-%m-%d"), + } + for encounter_id, timestamp in encounters.items() + ] + + # Sort by admission date + encounter_list.sort(key=lambda x: x["admission_date"]) + + return encounter_list + + except Exception: + logger.error( + f"Error fetching encounters for patient ID {patient_id}", + exc_info=True, + ) + raise - Parameters - ---------- - patient_id : int - The patient ID. - Returns - ------- - List[Event] - The events for the patient. - """ - return ehr_data_manager.fetch_patient_events(patient_id) +# Singleton instance +ehr_data_manager = EHRDataManager() -def fetch_patient_encounters(patient_id: int) -> List[int]: - """Fetch encounters for a patient. +# Public interface functions +def init_lazy_df(directory: str) -> None: + """Initialize the LazyFrame with the given directory.""" + ehr_data_manager.init_lazy_df(directory) - Parameters - ---------- - patient_id : int - The patient ID. - Returns - ------- - List[int] - The encounters for the patient. - """ - if ehr_data_manager.lazy_df is None: - raise ValueError("Lazy DataFrame not initialized. Call init_lazy_df first.") +def fetch_recent_encounter_events(patient_id: int) -> List[Event]: + """Fetch recent encounter events for a patient.""" + return ehr_data_manager.fetch_recent_encounter_events(patient_id) - try: - filtered_df = ehr_data_manager.lazy_df.filter(pl.col("patient_id") == patient_id) - encounter_ids = ( - filtered_df - .select("encounter_id") - .unique() - .collect() - .get_column("encounter_id") - .cast(pl.Utf8) - .to_list() - ) - encounter_ids = [str(eid) if eid is not None else "" for eid in encounter_ids] - return encounter_ids - except Exception as e: - logger.error(f"Error fetching encounters for patient ID {patient_id}: {str(e)}") - raise + +def fetch_patient_events(patient_id: int) -> List[Event]: + """Fetch all events for a patient.""" + return ehr_data_manager.fetch_patient_events(patient_id) + + +def fetch_patient_events_by_type(patient_id: int, event_type: str) -> List[Event]: + """Fetch events filtered by event_type for a patient.""" + return ehr_data_manager.fetch_patient_events_by_type(patient_id, event_type) diff --git a/adrenaline/api/routes/patients.py b/adrenaline/api/routes/patients.py index 91a1b7b..97bd64a 100644 --- a/adrenaline/api/routes/patients.py +++ b/adrenaline/api/routes/patients.py @@ -7,12 +7,13 @@ from fastapi import APIRouter, Depends, HTTPException, status from motor.motor_asyncio import AsyncIOMotorDatabase -from api.patients.data import ClinicalNote, PatientData, QAPair +from api.patients.data import ClinicalNote, Event, PatientData, QAPair from api.patients.db import get_database from api.patients.ehr import ( + fetch_patient_encounters, fetch_patient_events, + fetch_patient_events_by_type, init_lazy_df, - fetch_patient_encounters, ) from api.users.auth import ( get_current_active_user, @@ -36,14 +37,51 @@ init_lazy_df(MEDS_DATA_DIR) -@router.get("/patient_data/encounters/{patient_id}", response_model=List[str]) +@router.get( + "/patient_data/{patient_id}/events/{event_type}", response_model=List[Event] +) +async def get_patient_events_by_type( + patient_id: int, + event_type: str, + db: AsyncIOMotorDatabase[Any] = Depends(get_database), # noqa: B008 + current_user: User = Depends(get_current_active_user), # noqa: B008 +) -> List[Event]: + """Retrieve events filtered by event_type for a specific patient. + + Parameters + ---------- + patient_id : int + The ID of the patient. + event_type : str + The type of event to filter by. + db : AsyncIOMotorDatabase + The database connection. + current_user : User + The current authenticated user. + + Returns + ------- + List[Event] + List of events of the specified type for the patient. + """ + try: + return fetch_patient_events_by_type(patient_id, event_type) + except Exception as e: + logger.error(f"Error retrieving events for patient ID {patient_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="An error occurred while retrieving patient events", + ) from e + + +@router.get("/patient_data/encounters/{patient_id}", response_model=List[dict]) async def get_encounters( patient_id: int, db: AsyncIOMotorDatabase[Any] = Depends(get_database), # noqa: B008 current_user: User = Depends(get_current_active_user), # noqa: B008 -) -> List[str]: +) -> List[dict]: """ - Retrieve a list of all unique encounters for a patient. + Retrieve encounters with admission dates for a patient. Parameters ---------- @@ -56,19 +94,20 @@ async def get_encounters( Returns ------- - List[str] - The list of unique encounters. + List[dict] + List of encounters with their admission dates. """ try: - encounters = fetch_patient_encounters(patient_id) - return encounters + return fetch_patient_encounters(patient_id) except HTTPException: raise except Exception as e: - logger.error(f"Error retrieving encounters for patient ID {patient_id}: {str(e)}") + logger.error( + f"Error retrieving encounters for patient ID {patient_id}: {str(e)}" + ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="An error occurred while retrieving patient data", + detail="An error occurred while retrieving patient encounters", ) from e diff --git a/scripts/create_instruction_answers.py b/scripts/create_instruction_answers.py index 15e09ae..e1a3747 100644 --- a/scripts/create_instruction_answers.py +++ b/scripts/create_instruction_answers.py @@ -1,10 +1,7 @@ """Script to create instruction answers for EHR data.""" import os -from api.patients.ehr import ( - init_lazy_df, - fetch_recent_encounter_events -) +from api.patients.ehr import init_lazy_df, fetch_recent_encounter_events MEDS_DATA_DIR = os.getenv( @@ -23,6 +20,3 @@ print(events_str) - - - diff --git a/scripts/test_openbiollm.py b/scripts/test_openbiollm.py index b7e8e28..91bf91f 100644 --- a/scripts/test_openbiollm.py +++ b/scripts/test_openbiollm.py @@ -5,7 +5,7 @@ from typing import Any # Set the base URL for the API -BASE_URL = "http://gpu039:8080/v1" +BASE_URL = "http://localhost:8080/v1" # Initialize the OpenAI client client = OpenAI(base_url=BASE_URL, api_key="EMPTY") @@ -16,7 +16,7 @@ @backoff.on_exception(backoff.expo, Exception, max_tries=3) def send_chat_prompt( - prompt: str, model: str = "Llama3-OpenBioLLM-70B", max_tokens: int = 1024 + prompt: str, model: str = "Meta-Llama-3.1-70B-Instruct", max_tokens: int = 1024 ) -> Any: """Send a prompt to the chat completions endpoint with retries.""" try: diff --git a/ui/src/app/components/events-table.tsx b/ui/src/app/components/events-table.tsx index b26920e..956f89a 100644 --- a/ui/src/app/components/events-table.tsx +++ b/ui/src/app/components/events-table.tsx @@ -1,8 +1,22 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useState, useCallback } from 'react' import { useTable, useSortBy, useFlexLayout } from 'react-table' import { - Table, Thead, Tbody, Tr, Th, Td, chakra, Badge, Box, useColorModeValue, - Text, Tooltip, VStack + Table, + Thead, + Tbody, + Tr, + Th, + Td, + chakra, + Badge, + Box, + useColorModeValue, + Text, + Tooltip, + VStack, + Select, + HStack, + Spinner } from '@chakra-ui/react' import { TriangleDownIcon, TriangleUpIcon } from '@chakra-ui/icons' import { Event } from '../types/patient' @@ -12,7 +26,52 @@ interface EventsTableProps { } const EventsTable: React.FC = ({ events }) => { - const data = useMemo(() => events, [events]) + const [selectedEventType, setSelectedEventType] = useState('all') + const [filteredEvents, setFilteredEvents] = useState(events) + const [isLoading, setIsLoading] = useState(false) + + // Get unique event types + const eventTypes = useMemo(() => { + const types = new Set(events.map(event => event.event_type)) + return ['all', ...Array.from(types)] + }, [events]) + + // Filter events when selection changes + const handleEventTypeChange = useCallback(async (value: string) => { + setIsLoading(true) + setSelectedEventType(value) + + if (value === 'all') { + setFilteredEvents(events) + setIsLoading(false) + return + } + + try { + const token = localStorage.getItem('token') + if (!token) throw new Error('No token found') + + const response = await fetch(`/api/patient_data/${events[0].patient_id}/events/${value}`, { + headers: { 'Authorization': `Bearer ${token}` }, + }) + + if (!response.ok) { + throw new Error('Failed to fetch filtered events') + } + + const filteredData = await response.json() + setFilteredEvents(filteredData) + } catch (error) { + console.error('Error filtering events:', error) + // Fallback to client-side filtering if API call fails + setFilteredEvents(events.filter(event => event.event_type === value)) + } finally { + setIsLoading(false) + } + }, [events]) + + const data = useMemo(() => filteredEvents, [filteredEvents]) + const columns = useMemo( () => [ { @@ -31,7 +90,7 @@ const EventsTable: React.FC = ({ events }) => { }, { Header: 'Event Type', - accessor: (row: Event) => row.code.split('//')[1], + accessor: 'event_type', width: 120, Cell: ({ value }: { value: string }) => ( @@ -39,19 +98,27 @@ const EventsTable: React.FC = ({ events }) => { ), }, + { + Header: 'Environment', + accessor: 'environment', + width: 100, + Cell: ({ value }: { value: string | null }) => ( + value ? ( + + {value} + + ) : null + ), + }, { Header: 'Details', - accessor: (row: Event) => row, + accessor: 'details', width: 250, - Cell: ({ value }: { value: Event }) => { - const eventType = value.code.split('//')[1] - const details = eventType === 'LAB' ? value.description : value.code.split('//').slice(1).join(', ') - return ( - - {details} - - ) - }, + Cell: ({ value }: { value: string }) => ( + + {value} + + ), }, { Header: 'Value', @@ -80,77 +147,99 @@ const EventsTable: React.FC = ({ events }) => { const bgColor = useColorModeValue('white', 'gray.800') const borderColor = useColorModeValue('gray.200', 'gray.600') const hoverBgColor = useColorModeValue('gray.50', 'gray.700') + const selectBgColor = useColorModeValue('white', 'gray.700') return ( - - - - {headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( - - ))} - + + + - - {rows.map((row) => { - prepareRow(row) - return ( - - {row.cells.map((cell) => ( - + {rows.map((row) => { + prepareRow(row) + return ( + + {row.cells.map((cell) => ( + + ))} + + )} + )} + +
- - {column.render('Header')} - - {column.isSorted ? ( - column.isSortedDesc ? ( - - ) : ( - - ) - ) : null} - - -
+ {isLoading && } + + + + + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + ))} - ) - })} - -
- {cell.render('Cell')} - + + {column.render('Header')} + + {column.isSorted ? ( + column.isSortedDesc ? ( + + ) : ( + + ) + ) : null} + + +
+ ))} + +
+ {cell.render('Cell')} +
+
) } diff --git a/ui/src/app/components/patient-encounters-table.tsx b/ui/src/app/components/patient-encounters-table.tsx new file mode 100644 index 0000000..cc55189 --- /dev/null +++ b/ui/src/app/components/patient-encounters-table.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import { + Card, + CardBody, + Heading, + TableContainer, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Skeleton, + Text, + useColorModeValue +} from '@chakra-ui/react' + +interface Encounter { + encounter_id: string; + admission_date: string; +} + +interface PatientEncountersTableProps { + encounters: Encounter[]; + isLoading: boolean; +} + +const PatientEncountersTable: React.FC = ({ + encounters, + isLoading +}) => { + const cardBgColor = useColorModeValue('white', 'gray.800') + const borderColor = useColorModeValue('gray.200', 'gray.600') + const headerBgColor = useColorModeValue('gray.50', 'gray.700') + + return ( + + + + Patient Encounters + + {isLoading ? ( + + ) : encounters.length > 0 ? ( + + + + + + + + + + {encounters.map((encounter, index) => ( + + + + + ))} + +
Encounter IDAdmission Date
{encounter.encounter_id}{new Date(encounter.admission_date).toLocaleDateString()}
+
+ ) : ( + No encounters found + )} +
+
+ ) +} + +export default PatientEncountersTable diff --git a/ui/src/app/components/patient-summary-card.tsx b/ui/src/app/components/patient-summary-card.tsx index 8bf614a..9634d6f 100644 --- a/ui/src/app/components/patient-summary-card.tsx +++ b/ui/src/app/components/patient-summary-card.tsx @@ -1,46 +1,89 @@ import React from 'react' import { - Card, CardBody, Heading, Text, VStack, HStack, Icon, useColorModeValue + Card, + CardBody, + Heading, + Text, + VStack, + HStack, + Icon, + useColorModeValue, + Divider } from '@chakra-ui/react' -import { FaUserAlt, FaCalendarAlt, FaVenusMars } from 'react-icons/fa' +import { + FaUserAlt, + FaCalendarAlt, + FaVenusMars, + FaClipboardList, + FaNotesMedical +} from 'react-icons/fa' import { PatientData } from '../types/patient' interface PatientSummaryCardProps { patientData: PatientData } +const SummaryItem: React.FC<{ + icon: React.ElementType; + label: string; + value: string | number; +}> = ({ icon, label, value }) => ( + + + {label}: + {value} + +) + const PatientSummaryCard: React.FC = ({ patientData }) => { const cardBgColor = useColorModeValue('white', 'gray.800') const borderColor = useColorModeValue('gray.200', 'gray.600') return ( - + - Patient Summary - - - ID: - {patientData.patient_id} - - - - Age: - {patientData.age} - - - - Gender: - {patientData.gender} - - - Events: - {patientData.events?.length || 0} - - - Notes: - {patientData.notes?.length || 0} - + + Patient Summary + + + + + + + diff --git a/ui/src/app/patient/[id]/page.tsx b/ui/src/app/patient/[id]/page.tsx index b3cba43..9732e9f 100644 --- a/ui/src/app/patient/[id]/page.tsx +++ b/ui/src/app/patient/[id]/page.tsx @@ -3,19 +3,19 @@ import React, { useEffect, useState, useCallback } from 'react' import { useParams } from 'next/navigation' import { - Box, - Flex, - VStack, - useColorModeValue, - Container, - Card, + Box, + Flex, + VStack, + useColorModeValue, + Container, + Card, CardBody, - useToast, - Skeleton, - Text, - Grid, - GridItem, - Progress, + useToast, + Skeleton, + Text, + Grid, + GridItem, + Progress, Heading, Table, Thead, @@ -31,6 +31,7 @@ import { withAuth } from '../../components/with-auth' import { PatientData } from '../../types/patient' import PatientSummaryCard from '../../components/patient-summary-card' import PatientDetailsCard from '../../components/patient-details-card' +import PatientEncountersTable from '../../components/patient-encounters-table' import SearchBox from '../../components/search-box' import AnswerCard from '../../components/answer-card' @@ -195,40 +196,6 @@ const PatientPage: React.FC = () => { const { isSearching, answer, reasoning, pageId } = searchState - const EncountersTable = () => ( - - - - Patient Encounters - - {isLoadingEncounters ? ( - - ) : encounters.length > 0 ? ( - - - - - - - - - - {encounters.map((encounter, index) => ( - - - - - ))} - -
Encounter IDDate
{encounter}{new Date(encounter.split('_')[1]).toLocaleDateString()}
-
- ) : ( - No encounters found - )} -
-
- ) - return ( @@ -257,10 +224,13 @@ const PatientPage: React.FC = () => { )} - - + + - + {isSearching && ( { ) } -export default withAuth(PatientPage) \ No newline at end of file +export default withAuth(PatientPage) diff --git a/ui/src/app/types/patient.ts b/ui/src/app/types/patient.ts index 505c609..4b7571f 100644 --- a/ui/src/app/types/patient.ts +++ b/ui/src/app/types/patient.ts @@ -24,8 +24,11 @@ export interface ClinicalNote { code: string; description: string; timestamp: string; - numeric_value: number; - text_value: string; + numeric_value?: number; + text_value?: string; + event_type: string; + environment: string | null; + details: string; } export interface MetaAnnotation {