diff --git a/package.json b/package.json index e511cd5..78dd7cf 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@mui/icons-material": "^5.14.18", "@mui/joy": "^5.0.0-beta.15", "@mui/material": "^5.14.18", + "@mui/x-charts": "^6.18.3", "@mui/x-data-grid": "^6.18.1", "@mui/x-data-grid-pro": "^6.18.1", "@mui/x-license-pro": "^6.10.2", diff --git a/src/app/core/patents/actions.ts b/src/app/core/patents/actions.ts index 12d39ee..b2c1548 100644 --- a/src/app/core/patents/actions.ts +++ b/src/app/core/patents/actions.ts @@ -4,6 +4,7 @@ import { cache } from 'react'; import { z } from 'zod'; import { + ENTITY_SEARCH_API_URL, PATENT_AUTOCOMPLETE_API_URL, PATENT_SEARCH_API_URL, TERM_DESCRIPTION_API_URL, @@ -22,6 +23,11 @@ import { TrialResponseSchema, TrialSearchArgs, } from '@/types/trials'; +import { + EntityResponse, + EntityResponseSchema, + EntitySearchArgs, +} from '@/types/entities'; const AutocompleteResponse = z.array( z.object({ @@ -69,6 +75,26 @@ export const fetchDescription = cache( } ); +/** + * Fetch entities from the API. Cached. + * @param args + * @returns patents promise + */ +export const fetchEntities = cache( + async (args: EntitySearchArgs): Promise => { + if (args.terms?.length === 0) { + return []; + } + const queryArgs = getQueryArgs(args, true); + const res = await doFetch( + `${ENTITY_SEARCH_API_URL}?${queryArgs}`, + EntityResponseSchema + ); + + return res; + } +); + /** * Fetch patents from the API. Cached. * @param args diff --git a/src/app/core/patents/compound.tsx b/src/app/core/patents/compound.tsx new file mode 100644 index 0000000..28ecac4 --- /dev/null +++ b/src/app/core/patents/compound.tsx @@ -0,0 +1,98 @@ +'use server'; + +import Box from '@mui/joy/Box'; +import { GridColDef } from '@mui/x-data-grid/models/colDef'; +import Alert from '@mui/joy/Alert'; +import Typography from '@mui/joy/Typography'; +import WarningIcon from '@mui/icons-material/Warning'; +import 'server-only'; + +import { + DataGrid, + renderChip, + renderCompoundCountChip, + renderSparkline, +} from '@/components/data/grid'; +import { EntitySearchArgs } from '@/types/entities'; + +import { fetchEntities } from './actions'; + +const getCompoundColumns = (): GridColDef[] => [ + { + field: 'name', + headerName: 'Entity', + width: 250, + }, + { + field: 'trial_count', + headerName: 'Trials', + width: 125, + renderCell: renderCompoundCountChip, + }, + { + field: 'patent_count', + headerName: 'Patents', + width: 125, + renderCell: renderCompoundCountChip, + }, + { + field: 'activity', + headerName: 'Activity', + width: 125, + renderCell: renderSparkline, + }, + { + field: 'last_priority_year', + headerName: 'Latest Priority Date', + width: 125, + }, + { + field: 'max_phase', + headerName: 'Max Phase', + width: 125, + renderCell: renderChip, + }, + { + field: 'last_status', + headerName: 'Last Status', + width: 125, + renderCell: renderChip, + }, + { + field: 'last_updated', + headerName: 'Last Update', + width: 125, + }, +]; + +export const CompoundList = async (args: EntitySearchArgs) => { + const columns = getCompoundColumns(); + try { + const entities = await fetchEntities(args); + return ( + + } + rows={entities.map((entity) => ({ + ...entity, + id: entity.name, + }))} + /> + + ); + } catch (e) { + return ( + } + variant="soft" + color="warning" + > + Failed to fetch patents + + {e instanceof Error ? e.message : JSON.stringify(e)} + + + ); + } +}; diff --git a/src/app/core/patents/content.tsx b/src/app/core/patents/content.tsx index 637b9c7..064b0b6 100644 --- a/src/app/core/patents/content.tsx +++ b/src/app/core/patents/content.tsx @@ -11,6 +11,7 @@ import { Tabs } from '@/components/layout/tabs'; import { PatentSearchArgs } from '@/types/patents'; import { getStyles } from './client'; +import { CompoundList } from './compound'; import { PatentList } from './patent'; import { PatentGraph } from './graph'; import { OverTime } from './over-time'; @@ -20,6 +21,14 @@ import { TrialList } from './trials'; export const Content = (args: PatentSearchArgs) => { try { const tabs = [ + { + label: 'Compounds', + panel: ( + }> + + + ), + }, { label: 'Patents', panel: ( diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 929deb1..5e87ea1 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -9,6 +9,7 @@ import { } from '@mui/x-data-grid/models/params/gridCellParams'; import unescape from 'lodash/fp/unescape'; import { format } from 'date-fns'; +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; import { Chip, ChipProps, formatChips } from '@/components/data/chip'; import { formatLabel, formatPercent, title } from '@/utils/string'; @@ -152,18 +153,55 @@ export const renderList = ( * Render string array as chips */ export const getRenderChip = - (color: ChipProps['color']) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (params: GridRenderCellParams): ReactNode => { - const { value } = params; - if (typeof value !== 'string') { + >( + color: ChipProps['color'], + getUrl: (row: T) => string | undefined = () => undefined + ) => + (params: GridRenderCellParams): ReactNode => { + const { value, row } = params; + if (typeof value !== 'string' && typeof value !== 'number') { return <>{JSON.stringify(value)}; } - return {formatLabel(value || '')}; + + const href = getUrl(row); + + if (!value) { + return ; + } + + return ( + + {formatLabel(value || '')} + + ); }; export const renderPrimaryChip = getRenderChip('primary'); export const renderChip = getRenderChip('neutral'); +export const renderCompoundCountChip = getRenderChip( + 'primary', + (row: { name: string }) => `/core/patents?terms=${row.name}` +); + +export const getRenderSparkline = + >() => + (params: GridRenderCellParams): ReactNode => { + const { value } = params; + if (!value) { + return ; + } + return ( + + ); + }; + +export const renderSparkline = getRenderSparkline(); /** * Format label diff --git a/src/constants.ts b/src/constants.ts index a4c1390..a84bb8a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ export const API_URL = 'http://localhost:3001/dev'; // 'https://api.biosymbolics.ai'; export const API_KEY = 'Ahu8ef3VoNzBqn'; // /biosymbolics/pipeline/api/free-key +export const ENTITY_SEARCH_API_URL = `${API_URL}/entities/search`; export const PATENT_SEARCH_API_URL = `${API_URL}/patents/search`; export const PATENT_GRAPH_API_URL = `${API_URL}/patents/reports/graph`; export const PATENT_SUMMARY_API_URL = `${API_URL}/patents/reports/summarize`; diff --git a/src/types/entities.ts b/src/types/entities.ts new file mode 100644 index 0000000..7e1980a --- /dev/null +++ b/src/types/entities.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { z } from 'zod'; + +import { PatentSchema } from './patents'; +import { TrialSchema } from './trials'; + +export const EntitySchema = z.object({ + activity: z.array(z.number()), + last_status: z.union([z.string(), z.null()]), + last_updated: z.union([z.string(), z.null()]), + name: z.string(), + max_phase: z.union([z.string(), z.null()]), + last_priority_year: z.union([z.number(), z.null()]), + patents: z.array(PatentSchema), + patent_count: z.number(), + record_count: z.number(), + trials: z.array(TrialSchema), + trial_count: z.number(), +}); + +export const EntityResponseSchema = z.array(EntitySchema); + +export type Entity = z.infer; +export type EntityResponse = z.infer; + +export type EntitySearchArgs = { + queryType: string | null; + terms: string[] | null; +}; diff --git a/yarn.lock b/yarn.lock index 901f812..0ef4d60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,6 +69,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.23.5": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" + integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/types@^7.22.5": version "7.22.11" resolved "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz" @@ -281,6 +288,19 @@ clsx "^2.0.0" prop-types "^15.8.1" +"@mui/base@^5.0.0-beta.22": + version "5.0.0-beta.27" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.27.tgz#21a9c7d954a5f88f6706dfee630154c651ee73af" + integrity sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA== + dependencies: + "@babel/runtime" "^7.23.5" + "@floating-ui/react-dom" "^2.0.4" + "@mui/types" "^7.2.11" + "@mui/utils" "^5.15.0" + "@popperjs/core" "^2.11.8" + clsx "^2.0.0" + prop-types "^15.8.1" + "@mui/core-downloads-tracker@^5.14.18": version "5.14.18" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.18.tgz#f8b187dc89756fa5c0b7d15aea537a6f73f0c2d8" @@ -358,6 +378,11 @@ csstype "^3.1.2" prop-types "^15.8.1" +"@mui/types@^7.2.11": + version "7.2.11" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.11.tgz#36b99a88f8010dc716128e568dc05681a69dc7ae" + integrity sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w== + "@mui/types@^7.2.9": version "7.2.9" resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.9.tgz#730ee83a37af292a5973962f78ce5c95f31213a7" @@ -383,6 +408,31 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/utils@^5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.0.tgz#87b4db92dd2ddf3e2676377427f50662124013b4" + integrity sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA== + dependencies: + "@babel/runtime" "^7.23.5" + "@types/prop-types" "^15.7.11" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-charts@^6.18.3": + version "6.18.3" + resolved "https://registry.yarnpkg.com/@mui/x-charts/-/x-charts-6.18.3.tgz#d99504b59d55a4e45ed13b7f55b21fe98e4c6714" + integrity sha512-RpYaXGdFKxFW5xH+Lahkte9ew1Pawd/fQQmvgQPB7ufUH9PibOpaqxIV3gZvOMexMSmPXWCTHRucQkce9goNTA== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/base" "^5.0.0-beta.22" + "@react-spring/rafz" "^9.7.3" + "@react-spring/web" "^9.7.3" + clsx "^2.0.0" + d3-color "^3.1.0" + d3-scale "^4.0.2" + d3-shape "^3.2.0" + prop-types "^15.8.1" + "@mui/x-data-grid-pro@^6.18.1": version "6.18.1" resolved "https://registry.yarnpkg.com/@mui/x-data-grid-pro/-/x-data-grid-pro-6.18.1.tgz#24ac7bb281729ce84f52f8048d222c572c3ff434" @@ -499,6 +549,50 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@react-spring/animated@~9.7.3": + version "9.7.3" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.3.tgz#4211b1a6d48da0ff474a125e93c0f460ff816e0f" + integrity sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw== + dependencies: + "@react-spring/shared" "~9.7.3" + "@react-spring/types" "~9.7.3" + +"@react-spring/core@~9.7.3": + version "9.7.3" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.3.tgz#60056bcb397f2c4f371c6c9a5f882db77ae90095" + integrity sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ== + dependencies: + "@react-spring/animated" "~9.7.3" + "@react-spring/shared" "~9.7.3" + "@react-spring/types" "~9.7.3" + +"@react-spring/rafz@^9.7.3": + version "9.7.3" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.3.tgz#d6a9695c581f58a49e047ff7ed5646e0b681396c" + integrity sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ== + +"@react-spring/shared@~9.7.3": + version "9.7.3" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.3.tgz#4cf29797847c689912aec4e62e34c99a4d5d9e53" + integrity sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA== + dependencies: + "@react-spring/types" "~9.7.3" + +"@react-spring/types@~9.7.3": + version "9.7.3" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.3.tgz#ea78fd447cbc2612c1f5d55852e3c331e8172a0b" + integrity sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw== + +"@react-spring/web@^9.7.3": + version "9.7.3" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.3.tgz#d9f4e17fec259f1d65495a19502ada4f5b57fa3d" + integrity sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg== + dependencies: + "@react-spring/animated" "~9.7.3" + "@react-spring/core" "~9.7.3" + "@react-spring/shared" "~9.7.3" + "@react-spring/types" "~9.7.3" + "@rushstack/eslint-patch@^1.3.3": version "1.5.1" resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz" @@ -591,6 +685,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.10.tgz#892afc9332c4d62a5ea7e897fe48ed2085bbb08a" integrity sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A== +"@types/prop-types@^15.7.11": + version "15.7.11" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" + integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== + "@types/react-cytoscapejs@^1.2.5": version "1.2.5" resolved "https://registry.npmjs.org/@types/react-cytoscapejs/-/react-cytoscapejs-1.2.5.tgz" @@ -1199,6 +1298,67 @@ cytoscape@^3.27.0: heap "^0.2.6" lodash "^4.17.21" +"d3-array@2 - 3", "d3-array@2.10.0 - 3": + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3", d3-color@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" @@ -2113,6 +2273,11 @@ internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + is-alphabetical@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz"