Skip to content

Commit 59206e7

Browse files
committed
feat(app): show documents on main timeline, show participants on cards
1 parent 7690f28 commit 59206e7

File tree

7 files changed

+479
-340
lines changed

7 files changed

+479
-340
lines changed

Diff for: apps/web/src/components/hooks/useRecordQuery.tsx

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { BundleEntry, FhirResource } from 'fhir/r2';
2+
import { useCallback, useEffect, useRef, useState } from 'react';
3+
import { useDebounceCallback } from '@react-hook/debounce';
4+
import { useLocalConfig } from '../providers/LocalConfigProvider';
5+
import { useRxDb } from '../providers/RxDbProvider';
6+
import { useUser } from '../providers/UserProvider';
7+
import { useVectors } from '../providers/vector-provider';
8+
import { ClinicalDocument } from '../../models/clinical-document/ClinicalDocument.type';
9+
import {
10+
QueryStatus,
11+
fetchRecords,
12+
fetchRecordsWithVectorSearch,
13+
PAGE_SIZE,
14+
} from '../../pages/TimelineTab';
15+
16+
/**
17+
* Fetches records from the database and groups them by date
18+
* @param query Query to execute
19+
* @param enableAISemanticSearch Enable vector based semantic search
20+
* @returns
21+
*/
22+
export function useRecordQuery(
23+
query: string,
24+
enableAISemanticSearch?: boolean,
25+
): {
26+
data:
27+
| Record<string, ClinicalDocument<BundleEntry<FhirResource>>[]>
28+
| undefined; // Data returned by query, grouped records by date
29+
status: QueryStatus;
30+
initialized: boolean; // Indicates whether the query has run at least once
31+
loadNextPage: () => void; // Function to load next page of results
32+
showIndividualItems: boolean; // Indicates whether to show individual items or group by date
33+
} {
34+
const db = useRxDb(),
35+
{ experimental__use_openai_rag } = useLocalConfig(),
36+
user = useUser(),
37+
hasRun = useRef(false),
38+
[status, setQueryStatus] = useState(QueryStatus.IDLE),
39+
[initialized, setInitialized] = useState(false),
40+
[data, setList] =
41+
useState<Record<string, ClinicalDocument<BundleEntry<FhirResource>>[]>>(),
42+
[currentPage, setCurrentPage] = useState(0),
43+
[showIndividualItems, setShowIndividualItems] = useState(false),
44+
vectorStorage = useVectors(),
45+
execQuery = useCallback(
46+
/**
47+
*
48+
* @param merge Merge results with existing results. If false, results overwrite existing results
49+
* @param loadMore Indicate whether this is an inital load or a load more query. Affects visual loading state
50+
*/
51+
async ({ loadMore = false }: { loadMore?: boolean }) => {
52+
setQueryStatus(QueryStatus.COMPLETE_HIDE_LOAD_MORE);
53+
54+
try {
55+
let isAiSearch = false;
56+
if (loadMore) {
57+
setQueryStatus(QueryStatus.LOADING_MORE);
58+
}
59+
60+
// Execute query
61+
let groupedRecords = await fetchRecords(
62+
db,
63+
user.id,
64+
query,
65+
loadMore ? currentPage + 1 : currentPage,
66+
);
67+
68+
if (vectorStorage) {
69+
if (experimental__use_openai_rag) {
70+
if (enableAISemanticSearch) {
71+
if (Object.keys(groupedRecords).length === 0) {
72+
setQueryStatus(QueryStatus.LOADING);
73+
// If no results, try AI search
74+
isAiSearch = true;
75+
groupedRecords = (
76+
await fetchRecordsWithVectorSearch({
77+
db,
78+
vectorStorage,
79+
query,
80+
numResults: 20,
81+
enableSearchAttachments: true,
82+
groupByDate: true,
83+
})
84+
).records;
85+
setQueryStatus(QueryStatus.COMPLETE_HIDE_LOAD_MORE);
86+
}
87+
}
88+
}
89+
}
90+
91+
// If load more, increment page. Otherwise, reset page to 0
92+
if (loadMore) {
93+
console.debug('TimelineTab: load next page: ', currentPage + 1);
94+
setCurrentPage(currentPage + 1);
95+
} else {
96+
setCurrentPage(0);
97+
console.debug('TimelineTab: reset page to 0');
98+
}
99+
100+
// Merge results with existing results or overwrite existing results
101+
if (loadMore) {
102+
setList({ ...data, ...groupedRecords });
103+
} else {
104+
setList(groupedRecords);
105+
}
106+
107+
// Set query status.
108+
// Complete indicates that there are no more results to load
109+
// Success indicates that there are more results to load
110+
if (
111+
Object.values(groupedRecords).reduce((a, b) => a + b.length, 0) <
112+
PAGE_SIZE
113+
) {
114+
setQueryStatus(QueryStatus.COMPLETE_HIDE_LOAD_MORE);
115+
} else {
116+
setQueryStatus(QueryStatus.SUCCESS);
117+
}
118+
119+
if (isAiSearch) {
120+
// disable paging for AI search
121+
setQueryStatus(QueryStatus.COMPLETE_HIDE_LOAD_MORE);
122+
}
123+
124+
setInitialized(true);
125+
if (query) {
126+
setShowIndividualItems(true);
127+
} else {
128+
setShowIndividualItems(false);
129+
}
130+
} catch (e) {
131+
console.error(e);
132+
setQueryStatus(QueryStatus.ERROR);
133+
}
134+
},
135+
[
136+
vectorStorage,
137+
db,
138+
user.id,
139+
query,
140+
currentPage,
141+
experimental__use_openai_rag,
142+
enableAISemanticSearch,
143+
data,
144+
],
145+
),
146+
debounceExecQuery = useDebounceCallback(
147+
() => execQuery({ loadMore: false }),
148+
experimental__use_openai_rag ? 1000 : 300,
149+
),
150+
loadNextPage = useDebounceCallback(
151+
() => execQuery({ loadMore: true }),
152+
300,
153+
);
154+
155+
useEffect(() => {
156+
if (!hasRun.current) {
157+
hasRun.current = true;
158+
setQueryStatus(QueryStatus.LOADING);
159+
execQuery({
160+
loadMore: false,
161+
});
162+
}
163+
}, [execQuery, query]);
164+
165+
useEffect(() => {
166+
console.debug(
167+
'TimelineTab: query changed or AI toggled: ',
168+
query,
169+
enableAISemanticSearch,
170+
);
171+
setQueryStatus(QueryStatus.LOADING);
172+
debounceExecQuery();
173+
}, [query, debounceExecQuery, enableAISemanticSearch]);
174+
175+
return { data, status, initialized, loadNextPage, showIndividualItems };
176+
}

Diff for: apps/web/src/components/timeline/DiagnosticReportCard.tsx

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import { format, parseISO } from 'date-fns';
22
import { BundleEntry, DiagnosticReport, Observation } from 'fhir/r2';
3-
import { memo, useEffect, useMemo, useRef, useState } from 'react';
3+
import { memo, useEffect, useRef, useState } from 'react';
4+
import { RxDatabase, RxDocument } from 'rxdb';
5+
46
import { ClinicalDocument } from '../../models/clinical-document/ClinicalDocument.type';
5-
import { ShowDiagnosticReportResultsExpandable } from './ShowDiagnosticReportResultsExpandable';
6-
import { useConnectionDoc } from '../hooks/useConnectionDoc';
7+
import { ConnectionDocument } from '../../models/connection-document/ConnectionDocument.type';
8+
import { UserDocument } from '../../models/user-document/UserDocument.type';
9+
import { ButtonLoadingSpinner } from '../connection/ButtonLoadingSpinner';
710
import { CardBase } from '../connection/CardBase';
11+
import { useConnectionDoc } from '../hooks/useConnectionDoc';
812
import useIntersectionObserver from '../hooks/useIntersectionObserver';
9-
import { RxDatabase, RxDocument } from 'rxdb';
13+
import { DatabaseCollections } from '../providers/DatabaseCollections';
1014
import { useRxDb } from '../providers/RxDbProvider';
1115
import { useUser } from '../providers/UserProvider';
16+
import { AbnormalResultIcon } from './AbnormalResultIcon';
17+
import { isOutOfRangeResult } from './fhirpathParsers';
18+
import { OpenableCardIcon } from './OpenableCardIcon';
19+
import { ShowDiagnosticReportResultsExpandable } from './ShowDiagnosticReportResultsExpandable';
1220
import { SkeletonLoadingText } from './SkeletonLoadingText';
13-
import { TimelineCardTitle } from './TimelineCardTitle';
1421
import { TimelineCardCategoryTitle } from './TimelineCardCategoryTitle';
15-
import { ConnectionDocument } from '../../models/connection-document/ConnectionDocument.type';
16-
import { ButtonLoadingSpinner } from '../connection/ButtonLoadingSpinner';
17-
import { OpenableCardIcon } from './OpenableCardIcon';
1822
import { TimelineCardSubtitile } from './TimelineCardSubtitile';
19-
import { AbnormalResultIcon } from './AbnormalResultIcon';
20-
import { isOutOfRangeResult } from './fhirpathParsers';
21-
import { UserDocument } from '../../models/user-document/UserDocument.type';
22-
import { DatabaseCollections } from '../providers/DatabaseCollections';
23+
import { TimelineCardTitle } from './TimelineCardTitle';
2324

2425
/**
2526
* Function that encapsulates the logic of the useRelatedDocuments Hook.

0 commit comments

Comments
 (0)