enhavce search

This commit is contained in:
Ross
2026-07-11 15:19:16 +01:00
parent 1b395e7cd7
commit dea57ff45f
3 changed files with 705 additions and 546 deletions
+104 -536
View File
@@ -1,583 +1,151 @@
[ [
{ {
"path": "docs_md/articles/adrenal_082ca43c-db5c-4770-aeed-0c6ea317e8fc.md", "path": "docs_md/articles/intracranial-hemorrhage_3a9cbed6-aa2c-45a0-88bf-b39f1523ee85.md",
"title": "Adrenal", "title": "Intracranial Hemorrhage",
"docid": "082ca43c-db5c-4770-aeed-0c6ea317e8fc", "docid": "3a9cbed6-aa2c-45a0-88bf-b39f1523ee85",
"breadcrumbs": [ "breadcrumbs": [
"Genitourinary", "Pediatrics",
"Anatomy", "Differential Diagnosis",
"Adrenal" "Brain",
"Intracranial Hemorrhage"
], ],
"authors": [ "authors": [
{ {
"key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45", "key": "47381de4-c9fd-4999-8dd0-1808cd72db6b",
"value": "Siva P. Raman, MD" "value": "Luke L. Linscott, MD"
} }
], ],
"pageKeywords": "Genitourinary, Anatomy, Adrenal", "pageKeywords": "Pediatrics, Differential Diagnosis, Brain, Intracranial Hemorrhage",
"reasons": [ "reasons": [
"Content" "Content"
], ],
"snippet": "...hrine) into bloodstream - **Vessels**,**nerves**, and **lymphatics** - Arteries - **Superior adrenal arteries**: Typic..." "snippet": "...illness, past medical history - Preceding trauma may be obvious [e.g., motor vehicle accident (MVA)] or occult (e.g., child abuse),...",
}, "linked_info": {
{ "anatomy": {
"path": "docs_md/articles/sensorineural-hearing-loss-in-child_08c895da-f2aa-4076-abf0-af9aca1677cd.md", "exists": false,
"title": "Sensorineural Hearing Loss in Child", "links": []
"docid": "08c895da-f2aa-4076-abf0-af9aca1677cd",
"breadcrumbs": [
"Head and Neck",
"Differential Diagnosis",
"CPA-IAC and Posterior Fossa",
"Clinically Based Differentials",
"Sensorineural Hearing Loss in Child"
],
"authors": [
{
"key": "d19354f3-7ff2-495a-ad3f-064122e45602",
"value": "Bernadette L. Koch, MD"
}
],
"pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Clinically Based Differentials, Sensorineural Hearing Loss in Child",
"reasons": [
"Content"
],
"snippet": "...lar deficiency on CT - Look for large endolymphatic sac/duct ± cochlear malformation & modiolar deficiency on MR - **Conge..."
},
{
"path": "docs_md/articles/segmental-colonic-narrowing_c193bc86-43d2-4932-8378-5db8001d755e.md",
"title": "Segmental Colonic Narrowing",
"docid": "c193bc86-43d2-4932-8378-5db8001d755e",
"breadcrumbs": [
"Gastrointestinal",
"Differential Diagnosis",
"Colon",
"Generic Imaging Patterns",
"Segmental Colonic Narrowing"
],
"authors": [
{
"key": "6c5a9e0e-9dea-461b-9ad4-c00f5c4c2bbf",
"value": "Atif Zaheer, MD, FSAR"
}, },
{ "differential": {
"key": "e987d3d3-1206-48d6-824b-3347c2968855", "exists": false,
"value": "Michael P. Federle, MD, FACR" "links": []
} }
], }
"pageKeywords": "Gastrointestinal, Differential Diagnosis, Colon, Generic Imaging Patterns, Segmental Colonic Narrowing",
"reasons": [
"Content"
],
"snippet": "...etastases and Lymphoma** - Can be hematogenous or lymphatic spread or direct invasion - e.g., from duodenum, stomach, kidn..."
}, },
{ {
"path": "docs_md/articles/normal-pressure-hydrocephalus_ba3f857d-58de-4f21-8463-1631b4cb9972.md", "path": "docs_md/articles/epilepsy-child_a342e5b5-5b98-4003-a437-6d42a483b40e.md",
"title": "Normal-Pressure Hydrocephalus", "title": "Epilepsy, Child",
"docid": "ba3f857d-58de-4f21-8463-1631b4cb9972", "docid": "a342e5b5-5b98-4003-a437-6d42a483b40e",
"breadcrumbs": [ "breadcrumbs": [
"Brain", "Brain",
"Diagnosis",
"Anatomy-Based Diagnoses",
"Ventricles and Cisterns",
"Hydrocephalus",
"Normal-Pressure Hydrocephalus"
],
"authors": [
{
"key": "a25c450b-3d34-4f64-bba3-cc0834813df6",
"value": "Miral D. Jhaveri, MD, MBA"
}
],
"pageKeywords": "Brain, Diagnosis, Anatomy-Based Diagnoses, Ventricles and Cisterns, Hydrocephalus, Normal-Pressure Hydrocephalus",
"reasons": [
"Content"
],
"snippet": "...oval of neurotoxic compounds (β-amyloid & tau) - Glymphatic system - Facilitates brain fluid clearance & waste removal du..."
},
{
"path": "docs_md/articles/peritoneal-cavity_6691f48d-ac34-477b-8ec1-b9dd731a14a8.md",
"title": "Peritoneal Cavity",
"docid": "6691f48d-ac34-477b-8ec1-b9dd731a14a8",
"breadcrumbs": [
"Gastrointestinal",
"Anatomy",
"Gastrointestinal Tract and Abdominal Cavity",
"Peritoneal Cavity"
],
"authors": [
{
"key": "6c5a9e0e-9dea-461b-9ad4-c00f5c4c2bbf",
"value": "Atif Zaheer, MD, FSAR"
},
{
"key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45",
"value": "Siva P. Raman, MD"
},
{
"key": "e987d3d3-1206-48d6-824b-3347c2968855",
"value": "Michael P. Federle, MD, FACR"
}
],
"pageKeywords": "Gastrointestinal, Anatomy, Gastrointestinal Tract and Abdominal Cavity, Peritoneal Cavity",
"reasons": [
"Content"
],
"snippet": "...ntains **superior mesenteric artery and vein**, nerves, and lymphatics - **Transverse mesocolon**crosses almost horizontally in front of pancreas,..."
},
{
"path": "docs_md/articles/sensorineural-hearing-loss-in-adult_08d468da-fbc3-44f8-8212-6480e0a152c4.md",
"title": "Sensorineural Hearing Loss in Adult",
"docid": "08d468da-fbc3-44f8-8212-6480e0a152c4",
"breadcrumbs": [
"Head and Neck",
"Differential Diagnosis", "Differential Diagnosis",
"CPA-IAC and Posterior Fossa", "Brain Parenchyma, General",
"Clinically Based Differentials", "Clinically Based Differentials",
"Sensorineural Hearing Loss in Adult" "Epilepsy, Child"
], ],
"authors": [ "authors": [
{ {
"key": "eef2f839-5706-47b9-89c3-60d8315b2b3a", "key": "47381de4-c9fd-4999-8dd0-1808cd72db6b",
"value": "Nicholas A. Koontz, MD" "value": "Luke L. Linscott, MD"
} }
], ],
"pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Clinically Based Differentials, Sensorineural Hearing Loss in Adult", "pageKeywords": "Brain, Differential Diagnosis, Brain Parenchyma, General, Clinically Based Differentials, Epilepsy, Child",
"reasons": [ "reasons": [
"Content" "Content"
], ],
"snippet": "...ove; fat saturation suppresses fat signal - **Large Endolymphatic Sac Anomaly (IP-2)** - Most common lesion found in children with bilat..." "snippet": "...tes without recurrence does not require imaging - Trauma, remote stroke, or infection results in encephalomalacia &/or gliosis, which may ca...",
}, "linked_info": {
{ "anatomy": {
"path": "docs_md/articles/abdominal-lymph-nodes_3ca98d42-20ef-48fe-9265-ebfe570ba54b.md", "exists": false,
"title": "Abdominal Lymph Nodes", "links": []
"docid": "3ca98d42-20ef-48fe-9265-ebfe570ba54b",
"breadcrumbs": [
"Ultrasound",
"Anatomy",
"Abdomen",
"Abdominal Lymph Nodes"
],
"authors": [
{
"key": "07469ec4-05aa-4d65-a788-08b4d64048af",
"value": "Jade Wong-You-Cheong, MBChB, MRCP, FRCR, FSRU, FSAR"
}
],
"pageKeywords": "Ultrasound, Anatomy, Abdomen, Abdominal Lymph Nodes",
"reasons": [
"Content"
],
"snippet": "# GROSS ANATOMY - ## Overview - Major lymphatic vessels and nodal chains lie along major blood vessels [aorta, inferior vena cav..."
},
{
"path": "docs_md/articles/temporal-bone-facial-nerve-venous-malformation-hemangioma_dcd6a44e-cbe6-457c-9b03-598a2b874ece.md",
"title": "Temporal Bone Facial Nerve Venous Malformation (Hemangioma)",
"docid": "dcd6a44e-cbe6-457c-9b03-598a2b874ece",
"breadcrumbs": [
"Head and Neck",
"Diagnosis",
"Temporal Bone",
"Intratemporal Facial Nerve",
"Benign and Malignant Tumors",
"Temporal Bone Facial Nerve Venous Malformation (Hemangioma)"
],
"authors": [
{
"key": "07a2c087-6202-49e7-870b-7aa162d18f06",
"value": "Bronwyn E. Hamilton, MD"
}, },
{ "differential": {
"key": "33151213-01b2-4542-9105-342e006b3915", "exists": false,
"value": "H. Ric Harnsberger, MD" "links": []
} }
], }
"pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Intratemporal Facial Nerve, Benign and Malignant Tumors, Temporal Bone Facial Nerve Venous Malformation (Hemangioma)",
"reasons": [
"Content"
],
"snippet": "...* staining utilizing D2-40 antibody **negativity** excludes lymphatic malformation - ## Clinical Issues - Intratemporal FNVM produces **peri..."
}, },
{ {
"path": "docs_md/articles/colonic-wall-thickening_71ca9b30-6186-4129-957b-3b686b91b327.md", "path": "docs_md/articles/abusive-head-trauma_c57982e3-fa8f-4fd6-9184-04fd1d37a906.md",
"title": "Colonic Wall Thickening", "title": "Abusive Head Trauma",
"docid": "71ca9b30-6186-4129-957b-3b686b91b327", "docid": "c57982e3-fa8f-4fd6-9184-04fd1d37a906",
"breadcrumbs": [
"Gastrointestinal",
"Differential Diagnosis",
"Colon",
"Generic Imaging Patterns",
"Colonic Wall Thickening"
],
"authors": [
{
"key": "6c5a9e0e-9dea-461b-9ad4-c00f5c4c2bbf",
"value": "Atif Zaheer, MD, FSAR"
},
{
"key": "e987d3d3-1206-48d6-824b-3347c2968855",
"value": "Michael P. Federle, MD, FACR"
}
],
"pageKeywords": "Gastrointestinal, Differential Diagnosis, Colon, Generic Imaging Patterns, Colonic Wall Thickening",
"reasons": [
"Content"
],
"snippet": "...n with regional adenopathy (strong predictor of cancer with lymphatic spread) - Look for hepatic and peritoneal metastases - **Portal Hy..."
},
{
"path": "docs_md/articles/lymphangioma-mesenteric-cyst_bb42a128-c819-4368-a085-232b6db3434c.md",
"title": "Lymphangioma (Mesenteric Cyst)",
"docid": "bb42a128-c819-4368-a085-232b6db3434c",
"breadcrumbs": [
"Gastrointestinal",
"Diagnosis",
"Peritoneum, Mesentery, and Abdominal Wall",
"Benign Neoplasms",
"Lymphangioma (Mesenteric Cyst)"
],
"authors": [
{
"key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45",
"value": "Siva P. Raman, MD"
}
],
"pageKeywords": "Gastrointestinal, Diagnosis, Peritoneum, Mesentery, and Abdominal Wall, Benign Neoplasms, Lymphangioma (Mesenteric Cyst)",
"reasons": [
"Content"
],
"snippet": "...ymphoepithelial cyst, cystic lymphangioma, mesenteric cyst, lymphatic malformation - ## Definitions - Generic descriptive term used for beni..."
},
{
"path": "docs_md/articles/vertebral-body-t1-hypointense-signal-diffuse_5f4ceb8f-867f-4b4d-8149-57a906489453.md",
"title": "Vertebral Body, T1-Hypointense Signal, Diffuse",
"docid": "5f4ceb8f-867f-4b4d-8149-57a906489453",
"breadcrumbs": [
"Spine",
"Differential Diagnosis",
"Vertebral Body, Posterior Elements",
"Modality-Specific Imaging Findings",
"Vertebral Body, T1-Hypointense Signal, Diffuse"
],
"authors": [
{
"key": "bee1f359-33fb-4cba-9e6b-ed1ca1842439",
"value": "Jeffrey S. Ross, MD"
},
{
"key": "99e1aff7-f42c-43a0-95ae-d89c8551aa01",
"value": "Kevin R. Moore, MD"
}
],
"pageKeywords": "Spine, Differential Diagnosis, Vertebral Body, Posterior Elements, Modality-Specific Imaging Findings, Vertebral Body, T1-Hypointense Signal, Diffuse",
"reasons": [
"Content"
],
"snippet": "...nation (arterial or venous via Batson plexus) > perineural, lymphatic, CSF spread - Marrow initially infiltrated, trabeculae destroy..."
},
{
"path": "docs_md/articles/penetrating-atherosclerotic-ulcer_63f6cba2-2200-456a-8d03-ac3111e420c8.md",
"title": "Penetrating Atherosclerotic Ulcer",
"docid": "63f6cba2-2200-456a-8d03-ac3111e420c8",
"breadcrumbs": [
"Cardiac",
"Diagnosis",
"Aorta",
"Penetrating Atherosclerotic Ulcer"
],
"authors": [
{
"key": "9ad9af12-61a1-44d6-af52-0ee1f38eb298",
"value": "Davis Vigneault, MD, DPhil"
},
{
"key": "5de0df07-7b3e-4678-8767-1519e1153f29",
"value": "Dominik Fleischmann, MD"
},
{
"key": "10bb95ac-a27a-4ebe-833b-e59fea07734b",
"value": "Santiago Martínez-Jiménez, MD, FACR"
}
],
"pageKeywords": "Cardiac, Diagnosis, Aorta, Penetrating Atherosclerotic Ulcer",
"reasons": [
"Content"
],
"snippet": ".../ANATOMY:a7a252f0-2ac6-402a-8c87-cfce8adc799b ### Vessels, Lymphatic System, and Nerves, Abdominal Gastrointestinal/ANATOMY:0c38fd49-88e7-4272-960f-b..."
},
{
"path": "docs_md/articles/mycotic-aneurysm_20616d0a-7e0b-48d9-9be9-1af29e3dd6da.md",
"title": "Mycotic Aneurysm",
"docid": "20616d0a-7e0b-48d9-9be9-1af29e3dd6da",
"breadcrumbs": [
"Cardiac",
"Diagnosis",
"Aorta",
"Mycotic Aneurysm"
],
"authors": [
{
"key": "ee6ece9d-ad74-458c-a8df-11628ae7f879",
"value": "Arzu Canan, MD"
},
{
"key": "3d1e4c57-c1cf-4c89-b0f0-5d82b29a31e1",
"value": "Suhny Abbara, MD, FACR, MSCCT, FNASCI"
},
{
"key": "501b57b9-1723-4b1e-b21e-d4516655fac8",
"value": "Sanjeeva P. Kalva, MD, FSIR, FCIRSE"
}
],
"pageKeywords": "Cardiac, Diagnosis, Aorta, Mycotic Aneurysm",
"reasons": [
"Content"
],
"snippet": "...nal infection or intraabdominal abscess - Lymphatic spread - *Burkholderia pseudomallei* (causing melioidosis), endemic in..."
},
{
"path": "docs_md/articles/otosclerosis_ddc7b884-3c17-4834-9e96-d985c6b618a9.md",
"title": "Otosclerosis",
"docid": "ddc7b884-3c17-4834-9e96-d985c6b618a9",
"breadcrumbs": [
"Head and Neck",
"Diagnosis",
"Temporal Bone",
"Inner Ear",
"Infectious and Inflammatory Lesions",
"Otosclerosis"
],
"authors": [
{
"key": "07a2c087-6202-49e7-870b-7aa162d18f06",
"value": "Bronwyn E. Hamilton, MD"
},
{
"key": "4b6589b0-9b8d-4467-8a90-01a0a59742fc",
"value": "Troy A. Hutchins, MD"
}
],
"pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Inner Ear, Infectious and Inflammatory Lesions, Otosclerosis",
"reasons": [
"Content"
],
"snippet": "...kaida T et al: Magnetic resonance imaging evaluation of endolymphatic hydrops in cases with otosclerosis. Otol Neurotol. 36(7):1146-50, 2015](http://w..."
},
{
"path": "docs_md/articles/adrenal-cyst_c5d717a3-3d6e-4e86-9efe-1ad0ec14740f.md",
"title": "Adrenal Cyst",
"docid": "c5d717a3-3d6e-4e86-9efe-1ad0ec14740f",
"breadcrumbs": [
"Genitourinary",
"Diagnosis",
"Adrenal",
"Benign Neoplasms",
"Adrenal Cyst"
],
"authors": [
{
"key": "c3463c5c-31d3-4489-bbfe-6b895abdb86d",
"value": "Mitchell Tublin, MD"
}
],
"pageKeywords": "Genitourinary, Diagnosis, Adrenal, Benign Neoplasms, Adrenal Cyst",
"reasons": [
"Content"
],
"snippet": "...om preexisting vascular malformation or obstructed, ectatic lymphatic channels - Thin rim calcification typical - Epitheli..."
},
{
"path": "docs_md/articles/hydrocephalus_e9481739-278e-4682-ab1e-4326a77c3d0c.md",
"title": "Hydrocephalus",
"docid": "e9481739-278e-4682-ab1e-4326a77c3d0c",
"breadcrumbs": [ "breadcrumbs": [
"Pediatrics", "Pediatrics",
"Diagnosis", "Diagnosis",
"Pediatric Neuroradiology", "Pediatric Neuroradiology",
"Brain", "Brain",
"Anatomy-Based Diagnoses", "Pathology-Based Diagnoses",
"Ventricles and Cisterns", "Trauma",
"Hydrocephalus" "Abusive Head Trauma"
], ],
"authors": [ "authors": [
{ {
"key": "838e1722-2479-4fbd-a5fe-d965980a1a2c", "key": "47381de4-c9fd-4999-8dd0-1808cd72db6b",
"value": "Blaise V. Jones, MD" "value": "Luke L. Linscott, MD"
} }
], ],
"pageKeywords": "Pediatrics, Diagnosis, Pediatric Neuroradiology, Brain, Anatomy-Based Diagnoses, Ventricles and Cisterns, Hydrocephalus", "pageKeywords": "Pediatrics, Diagnosis, Pediatric Neuroradiology, Brain, Pathology-Based Diagnoses, Trauma, Abusive Head Trauma",
"reasons": [ "reasons": [
"Content" "Breadcrumbs",
"Content",
"Keywords",
"Title"
], ],
"snippet": "...Extracellular, extravascular fluid in brain is managed by glymphatic system - CSF circulates from within ventricles → subarachnoid space →..." "snippet": "# KEY FACTS - ## Terminology - Abusive head trauma (AHT) - Traumatic injury inflicted on infants & children by adults...",
}, "linked_info": {
{ "anatomy": {
"path": "docs_md/articles/inner-ear-lesion-adult_e5bbf757-d77a-4546-a848-d1a1a64cb230.md", "exists": true,
"title": "Inner Ear Lesion, Adult", "links": [
"docid": "e5bbf757-d77a-4546-a848-d1a1a64cb230", {
"breadcrumbs": [ "title": "Visual Network",
"Head and Neck", "docid": "404625d9-3125-4923-9f9d-53d0f81c3542"
"Differential Diagnosis", },
"Temporal Bone", {
"Anatomically Based Differentials", "title": "Anatomy Document",
"Inner Ear Lesion, Adult" "docid": "bf20450d-2629-4795-98e7-7788b665ad3c"
], },
"authors": [ {
{ "title": "Anatomy Document",
"key": "d19354f3-7ff2-495a-ad3f-064122e45602", "docid": "8fb2f541-799d-479f-b000-ab54f216199a"
"value": "Bernadette L. Koch, MD" }
} ]
],
"pageKeywords": "Head and Neck, Differential Diagnosis, Temporal Bone, Anatomically Based Differentials, Inner Ear Lesion, Adult",
"reasons": [
"Content"
],
"snippet": "...ochlear turns - Usually with enlarged endolymphatic sac/duct - Literature suggests dysplastic osseous spiral lamin..."
},
{
"path": "docs_md/articles/ventricles-and-cisterns-overview_ad860c4f-fe9a-4469-8eca-a7ccd5cff70f.md",
"title": "Ventricles and Cisterns Overview",
"docid": "ad860c4f-fe9a-4469-8eca-a7ccd5cff70f",
"breadcrumbs": [
"Brain",
"Diagnosis",
"Anatomy-Based Diagnoses",
"Ventricles and Cisterns",
"Ventricles and Cisterns Overview"
],
"authors": [
{
"key": "a25c450b-3d34-4f64-bba3-cc0834813df6",
"value": "Miral D. Jhaveri, MD, MBA"
}
],
"pageKeywords": "Brain, Diagnosis, Anatomy-Based Diagnoses, Ventricles and Cisterns, Ventricles and Cisterns Overview",
"reasons": [
"Content"
],
"snippet": "...lli along the superior sagittal sinus. CSF also drains into lymphatic vessels around the cranial cavity and spinal canal. - **Updated model of CSF and..."
},
{
"path": "docs_md/articles/takayasu-arteritis_d35eb6f3-bfd3-4121-8781-325a93ccc197.md",
"title": "Takayasu Arteritis",
"docid": "d35eb6f3-bfd3-4121-8781-325a93ccc197",
"breadcrumbs": [
"Cardiac",
"Diagnosis",
"Aorta",
"Takayasu Arteritis"
],
"authors": [
{
"key": "ee6ece9d-ad74-458c-a8df-11628ae7f879",
"value": "Arzu Canan, MD"
}, },
{ "differential": {
"key": "3d1e4c57-c1cf-4c89-b0f0-5d82b29a31e1", "exists": true,
"value": "Suhny Abbara, MD, FACR, MSCCT, FNASCI" "links": [
{
"title": "Benign Enlarged Subarachnoid Spaces",
"docid": "3da4fec0-6e87-4bcc-bd66-b4a5d1984f6e"
},
{
"title": "Mitochondrial Encephalopathies",
"docid": "40004435-b768-4baf-a31e-651f8a174fe2"
},
{
"title": "CSF Shunts and Complications",
"docid": "1027d634-92ff-47c1-8266-a7fc3acd1529"
},
{
"title": "Subdural Empyema",
"docid": "30b1f367-f047-4664-b34b-69b2d13867e0"
},
{
"title": "Differential Diagnosis",
"docid": "f2d6806c-a267-4f64-ba16-b0fa89e229b6"
},
{
"title": "Differential Diagnosis",
"docid": "6083739e-ec7b-48ad-9b34-80434c3142f2"
},
{
"title": "Differential Diagnosis",
"docid": "17ae265f-b471-4ca0-bba4-75b73a9d76f6"
},
{
"title": "Differential Diagnosis",
"docid": "da3780c6-b627-47c3-912d-6f4c452a1acd"
}
]
} }
], }
"pageKeywords": "Cardiac, Diagnosis, Aorta, Takayasu Arteritis",
"reasons": [
"Content"
],
"snippet": ".../ANATOMY:551f4b47-fac1-44f9-b800-09ce766fedd9 ### Vessels, Lymphatic System, and Nerves, Abdominal Gastrointestinal/ANATOMY:0c38fd49-88e7-4272-960f-b..."
},
{
"path": "docs_md/articles/peritoneal-spaces-and-structures_9c50ad6a-e96b-44a1-93d1-a4e7de5212c2.md",
"title": "Peritoneal Spaces and Structures",
"docid": "9c50ad6a-e96b-44a1-93d1-a4e7de5212c2",
"breadcrumbs": [
"Ultrasound",
"Anatomy",
"Abdomen",
"Peritoneal Spaces and Structures"
],
"authors": [
{
"key": "07469ec4-05aa-4d65-a788-08b4d64048af",
"value": "Jade Wong-You-Cheong, MBChB, MRCP, FRCR, FSRU, FSAR"
}
],
"pageKeywords": "Ultrasound, Anatomy, Abdomen, Peritoneal Spaces and Structures",
"reasons": [
"Content"
],
"snippet": "...to right; contains superior mesenteric vessels, nerves, and lymphatics - Transverse mesocolon crosses almost horizontally in front of pancreas, du..."
},
{
"path": "docs_md/articles/cpa-iac-lipoma_37ef2fb6-7d7e-48dc-b786-16ed4475b26e.md",
"title": "CPA-IAC Lipoma",
"docid": "37ef2fb6-7d7e-48dc-b786-16ed4475b26e",
"breadcrumbs": [
"Head and Neck",
"Diagnosis",
"CPA-IAC",
"Congenital Lesions",
"CPA-IAC Lipoma"
],
"authors": [
{
"key": "318f80ab-6abb-4067-a809-2ebdaa5a30c9",
"value": "Kalen Riley, MD, MBA"
},
{
"key": "e0282a62-994d-4550-a127-1eb773b1e920",
"value": "Blair A. Winegar, MD"
},
{
"key": "94f835c8-fa13-4e8a-995b-53048e6b0605",
"value": "Philip R. Chapman, MD"
}
],
"pageKeywords": "Head and Neck, Diagnosis, CPA-IAC, Congenital Lesions, CPA-IAC Lipoma",
"reasons": [
"Content"
],
"snippet": "...ant CPA-IAC lipoma with inner ear second fatty focus in endolymphatic duct-sac.\\n\\nThree axial CT (#1-3) and 3 axial MR (#4-6) images reveal an obviou..."
},
{
"path": "docs_md/articles/cystic-cpa-mass_6c60db6d-8093-4df5-8cbb-c6f6570ae167.md",
"title": "Cystic CPA Mass",
"docid": "6c60db6d-8093-4df5-8cbb-c6f6570ae167",
"breadcrumbs": [
"Head and Neck",
"Differential Diagnosis",
"CPA-IAC and Posterior Fossa",
"Generic Imaging Patterns",
"Cystic CPA Mass"
],
"authors": [
{
"key": "07a2c087-6202-49e7-870b-7aa162d18f06",
"value": "Bronwyn E. Hamilton, MD"
}
],
"pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Generic Imaging Patterns, Cystic CPA Mass",
"reasons": [
"Content"
],
"snippet": "...chnoid, & neurenteric cysts; neurocysticercosis & large endolymphatic sac anomaly - Many solid CPA tumors may have either intramural cysts,..."
},
{
"path": "docs_md/articles/cranial-meninges_3210cf7d-5be6-4741-a11d-dd7f8c1bc286.md",
"title": "Cranial Meninges",
"docid": "3210cf7d-5be6-4741-a11d-dd7f8c1bc286",
"breadcrumbs": [
"Brain",
"Anatomy",
"Scalp, Skull, and Meninges",
"Cranial Meninges"
],
"authors": [
{
"key": "5cff4116-3654-4b3a-bb75-5ebe0b8c9850",
"value": "Anne G. Osborn, MD, FACR"
}
],
"pageKeywords": "Brain, Anatomy, Scalp, Skull, and Meninges, Cranial Meninges",
"reasons": [
"Content"
],
"snippet": "...along penetrating arteries - Key part of brain \"glymphatic\" system ## GROSS ANATOMY - ### Overview - Brain encased by 3 meninge..."
} }
] ]
+189 -2
View File
@@ -20,7 +20,8 @@ import argparse
import csv import csv
import json import json
import os import os
from typing import List, Dict, Any import re
from typing import List, Dict, Any, Tuple, Optional
import yaml import yaml
import fnmatch import fnmatch
@@ -249,7 +250,135 @@ def match_author(authors: List[Dict[str, Any]], q: str) -> bool:
return False return False
def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: List[str] = None, or_queries: List[Dict[str, Any]] = None) -> List[Dict[str, Any]]: _docid_map = {}
def get_doc_by_id(root: str, identifier: str) -> Tuple[Optional[Dict[str, Any]], Optional[str], Optional[str]]:
global _docid_map
if not _docid_map:
for dirpath, dirnames, filenames in os.walk(root):
for fn in filenames:
if fn.endswith('.md'):
path = os.path.join(dirpath, fn)
fm, content = read_md_file(path)
basename = os.path.splitext(fn)[0]
if fm:
docid = fm.get('docid')
doc_info = (fm, content, path)
if docid:
_docid_map[docid] = doc_info
_docid_map[basename] = doc_info
return _docid_map.get(identifier, (None, None, None))
def extract_section_content(content: str, section_type: str) -> str:
lines = content.split('\n')
section_content = []
in_section = False
section_level = 0
if section_type == 'anatomy':
pattern = re.compile(r'^#+\s+.*anatomy.*', re.IGNORECASE)
elif section_type == 'differential':
pattern = re.compile(r'^#+\s+.*differential.*', re.IGNORECASE)
else:
return ""
for line in lines:
if pattern.match(line):
in_section = True
section_level = len(line) - len(line.lstrip('#'))
continue
if in_section:
if line.startswith('#'):
current_level = len(line) - len(line.lstrip('#'))
if current_level <= section_level:
in_section = False
continue
section_content.append(line)
return '\n'.join(section_content)
def find_links_in_text(text: str) -> List[Dict[str, str]]:
links = []
if not text:
return links
# 1. Standard markdown links [Label](/document/slug/uuid)
markdown_pattern = re.compile(r'\[([^\]]+)\]\((/document/[^/]+/([a-f0-9\-]{36}))\)')
for m in markdown_pattern.finditer(text):
links.append({
'title': m.group(1),
'docid': m.group(3)
})
# 2. Raw ANATOMY links (e.g. Brain/ANATOMY:uuid or ANATOMY:uuid)
anatomy_pattern = re.compile(r'(?:[a-zA-Z0-9\-]+/)?ANATOMY:([a-f0-9\-]{36})', re.IGNORECASE)
for m in anatomy_pattern.finditer(text):
docid = m.group(1)
if not any(l['docid'] == docid for l in links):
links.append({
'title': 'Anatomy Document',
'docid': docid
})
# 3. Raw DDX links (e.g. DDX:uuid)
ddx_pattern = re.compile(r'DDX:([a-f0-9\-]{36})', re.IGNORECASE)
for m in ddx_pattern.finditer(text):
docid = m.group(1)
if not any(l['docid'] == docid for l in links):
links.append({
'title': 'Differential Diagnosis',
'docid': docid
})
return links
def check_linked_sections(content: str, root: str = 'docs_md/articles') -> Dict[str, Any]:
anatomy_text = extract_section_content(content, 'anatomy')
anatomy_exists = bool(re.search(r'(?i)^#+\s+.*anatomy.*', content, re.MULTILINE))
diff_text = extract_section_content(content, 'differential')
diff_exists = bool(re.search(r'(?i)^#+\s+.*differential.*', content, re.MULTILINE))
raw_anatomy_links = find_links_in_text(anatomy_text)
raw_diff_links = find_links_in_text(diff_text)
anatomy_links = []
for link in raw_anatomy_links:
docid = link['docid']
fm, _, _ = get_doc_by_id(root, docid)
title = fm.get('title') or fm.get('pageTitle') if fm else link['title']
anatomy_links.append({
'title': title,
'docid': docid
})
diff_links = []
for link in raw_diff_links:
docid = link['docid']
fm, _, _ = get_doc_by_id(root, docid)
title = fm.get('title') or fm.get('pageTitle') if fm else link['title']
diff_links.append({
'title': title,
'docid': docid
})
return {
'anatomy': {
'exists': anatomy_exists,
'links': anatomy_links
},
'differential': {
'exists': diff_exists,
'links': diff_links
}
}
def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: List[str] = None, or_queries: List[Dict[str, Any]] = None, expand_links: bool = False) -> List[Dict[str, Any]]:
out = [] out = []
# Normalize queries list # Normalize queries list
@@ -456,7 +585,65 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li
'pageKeywords': fm.get('pageKeywords'), 'pageKeywords': fm.get('pageKeywords'),
'reasons': sorted(list(set(reasons))), 'reasons': sorted(list(set(reasons))),
'snippet': snippet, 'snippet': snippet,
'linked_info': check_linked_sections(content, root)
}) })
if expand_links:
seen_docids = {r.get('docid') for r in out if r.get('docid')}
seen_paths = {r['path'] for r in out}
expanded_results = []
for r in out:
info = r.get('linked_info')
if not info:
continue
parent_title = r['title']
# Anatomy links
for link in info['anatomy']['links']:
docid = link['docid']
if docid and docid not in seen_docids:
fm, content, path = get_doc_by_id(root, docid)
if fm:
linked_fm_title = fm.get('title') or fm.get('pageTitle') or link['title']
expanded_results.append({
'path': path,
'title': linked_fm_title,
'docid': docid,
'breadcrumbs': fm.get('breadcrumbs'),
'authors': fm.get('authors'),
'pageKeywords': fm.get('pageKeywords'),
'reasons': [f'Linked (Anatomy of {parent_title})'],
'snippet': content[:300] + '...' if content else '',
'linked_info': check_linked_sections(content, root)
})
seen_docids.add(docid)
seen_paths.add(path)
# Differential links
for link in info['differential']['links']:
docid = link['docid']
if docid and docid not in seen_docids:
fm, content, path = get_doc_by_id(root, docid)
if fm:
linked_fm_title = fm.get('title') or fm.get('pageTitle') or link['title']
expanded_results.append({
'path': path,
'title': linked_fm_title,
'docid': docid,
'breadcrumbs': fm.get('breadcrumbs'),
'authors': fm.get('authors'),
'pageKeywords': fm.get('pageKeywords'),
'reasons': [f'Linked (Diff Diag of {parent_title})'],
'snippet': content[:300] + '...' if content else '',
'linked_info': check_linked_sections(content, root)
})
seen_docids.add(docid)
seen_paths.add(path)
out.extend(expanded_results)
return out return out
+412 -8
View File
@@ -18,7 +18,11 @@ from typing import List, Dict, Any
import anyio import anyio
import asyncio import asyncio
from nicegui import ui from nicegui import ui, app
import re
app.add_static_files('/images', 'docs_md/articles/images')
app.add_static_files('/document/images', 'docs_md/articles/images')
import shutil import shutil
import subprocess import subprocess
@@ -156,6 +160,44 @@ def search_page() -> None: # build UI
color: #cbd5e1; color: #cbd5e1;
border: 1px solid rgba(148, 163, 184, 0.4); border: 1px solid rgba(148, 163, 184, 0.4);
} }
.badge-status-exists {
background-color: rgba(16, 185, 129, 0.15);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.35);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
display: inline-block;
}
.badge-status-exists.cursor-pointer {
cursor: pointer;
}
.badge-status-exists.cursor-pointer:hover {
background-color: rgba(16, 185, 129, 0.3) !important;
border-color: rgba(16, 185, 129, 0.6) !important;
text-decoration: underline;
}
.badge-status-missing {
background-color: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.35);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
display: inline-block;
}
.badge-status-zero {
background-color: rgba(59, 130, 246, 0.15);
color: #93c5fd;
border: 1px solid rgba(59, 130, 246, 0.35);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
display: inline-block;
}
</style> </style>
''') ''')
@@ -243,7 +285,9 @@ def search_page() -> None: # build UI
render_query_blocks() render_query_blocks()
with ui.row().classes('w-full justify-between items-center gap-4 mt-2'): with ui.row().classes('w-full justify-between items-center gap-4 mt-2'):
ui.button('Add OR Query', icon='add', on_click=add_query_block).classes('text-blue-400 border border-blue-800').props('outline dense') with ui.row().classes('items-center gap-4'):
ui.button('Add OR Query', icon='add', on_click=add_query_block).classes('text-blue-400 border border-blue-800').props('outline dense')
expand_chk = ui.checkbox('Expand with Linked Articles', value=False).classes('text-slate-300')
search_btn = ui.button('Search', icon='search', on_click=lambda: asyncio.create_task(run_search_handler())).classes('bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-8 rounded-lg shadow-md transition-all duration-300') search_btn = ui.button('Search', icon='search', on_click=lambda: asyncio.create_task(run_search_handler())).classes('bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-8 rounded-lg shadow-md transition-all duration-300')
# Advanced Criteria Expansion Box # Advanced Criteria Expansion Box
@@ -280,7 +324,8 @@ def search_page() -> None: # build UI
columns = [ columns = [
{'name': 'title', 'label': 'Title', 'field': 'title', 'required': True, 'align': 'left', 'sortable': True}, {'name': 'title', 'label': 'Title', 'field': 'title', 'required': True, 'align': 'left', 'sortable': True},
{'name': 'reasons', 'label': 'Matched In', 'field': 'reasons', 'align': 'left', 'sortable': True}, {'name': 'reasons', 'label': 'Matched In', 'field': 'reasons', 'align': 'left', 'sortable': True},
{'name': 'snippet', 'label': 'Snippet', 'field': 'snippet', 'align': 'left'}, {'name': 'anatomy', 'label': 'Anatomy', 'field': 'anatomy', 'align': 'center'},
{'name': 'differential', 'label': 'Diff Diagnosis', 'field': 'differential', 'align': 'center'},
{'name': 'docid', 'label': 'DocID', 'field': 'docid', 'align': 'left'}, {'name': 'docid', 'label': 'DocID', 'field': 'docid', 'align': 'left'},
{'name': 'path', 'label': 'Path', 'field': 'path', 'align': 'left', 'sortable': True}, {'name': 'path', 'label': 'Path', 'field': 'path', 'align': 'left', 'sortable': True},
] ]
@@ -300,10 +345,16 @@ def search_page() -> None: # build UI
# Add slot overrides to format table cells nicely # Add slot overrides to format table cells nicely
table.add_slot('body-cell-title', ''' table.add_slot('body-cell-title', '''
<q-td :props="props"> <q-td :props="props">
<a v-if="props.row.docid" :href="'https://app.statdx.com/document/' + props.row.docid" target="_blank" class="text-blue-400 hover:text-blue-300 font-semibold" style="text-decoration: none;"> <div class="row items-center no-wrap q-gutter-xs">
{{ props.row.title }} <a :href="'/document/' + (props.row.docid || props.row.path.split('/').pop().replace('.md', ''))" target="_blank" class="text-blue-400 hover:text-blue-300 font-semibold" style="text-decoration: none;">
</a> {{ props.row.title }}
<span v-else class="text-slate-200 font-semibold">{{ props.row.title }}</span> </a>
<q-btn v-if="props.row.linked_info && ((props.row.linked_info.anatomy && props.row.linked_info.anatomy.links && props.row.linked_info.anatomy.links.length > 0) || (props.row.linked_info.differential && props.row.linked_info.differential.links && props.row.linked_info.differential.links.length > 0))"
flat round dense size="xs" icon="alt_route" color="primary"
@click.stop="$parent.$emit('expand_row_links', props.row.path)">
<q-tooltip>Expand Linked Articles</q-tooltip>
</q-btn>
</div>
</q-td> </q-td>
''') ''')
table.add_slot('body-cell-reasons', ''' table.add_slot('body-cell-reasons', '''
@@ -315,6 +366,36 @@ def search_page() -> None: # build UI
</span> </span>
</q-td> </q-td>
''') ''')
table.add_slot('body-cell-anatomy', '''
<q-td :props="props" class="text-center">
<span v-if="!props.row.linked_info || !props.row.linked_info.anatomy.exists" class="badge-status-missing">
No Anatomy ❌
</span>
<span v-else-if="props.row.linked_info.anatomy.links.length > 0"
class="badge-status-exists cursor-pointer"
@click.stop="$parent.$emit('show_links_modal', { path: props.row.path, type: 'anatomy' })">
Anatomy ({{ props.row.linked_info.anatomy.links.length }}) 🔍
</span>
<span v-else class="badge-status-zero">
Anatomy (0)
</span>
</q-td>
''')
table.add_slot('body-cell-differential', '''
<q-td :props="props" class="text-center">
<span v-if="!props.row.linked_info || !props.row.linked_info.differential.exists" class="badge-status-missing">
No Diff Diag ❌
</span>
<span v-else-if="props.row.linked_info.differential.links.length > 0"
class="badge-status-exists cursor-pointer"
@click.stop="$parent.$emit('show_links_modal', { path: props.row.path, type: 'differential' })">
Diff Diag ({{ props.row.linked_info.differential.links.length }}) 🔍
</span>
<span v-else class="badge-status-zero">
Diff Diag (0)
</span>
</q-td>
''')
table.add_slot('body-cell-path', ''' table.add_slot('body-cell-path', '''
<q-td :props="props"> <q-td :props="props">
<span class="text-slate-400 text-xs"> <span class="text-slate-400 text-xs">
@@ -322,6 +403,205 @@ def search_page() -> None: # build UI
</span> </span>
</q-td> </q-td>
''') ''')
table.on('expand_row_links', lambda msg: asyncio.create_task(expand_row_links_handler(msg.args)))
table.on('show_links_modal', lambda msg: show_links_modal_handler(msg.args))
async def expand_row_links_handler(path: str):
with table.client:
row = None
for r in table.rows:
if r['path'] == path:
row = r
break
if not row:
return
info = row.get('linked_info')
if not info:
ui.notify('No link metadata found for this article', color='warning')
return
docids = []
origins = {}
for link in info['anatomy']['links']:
docids.append(link['docid'])
origins[link['docid']] = f"Linked (Anatomy of {row['title']})"
for link in info['differential']['links']:
docids.append(link['docid'])
origins[link['docid']] = f"Linked (Diff Diag of {row['title']})"
if not docids:
ui.notify('No linked articles in Anatomy or Diff Diagnosis sections', color='info')
return
status.set_text('Expanding linked articles...')
try:
resolved = []
seen_paths = {r['path'] for r in table.rows}
seen_docids = {r.get('docid') for r in table.rows if r.get('docid')}
def fetch_linked():
res = []
for docid in docids:
if docid and docid not in seen_docids:
fm, content, doc_path = search_md.get_doc_by_id('docs_md/articles', docid)
if fm:
linked_fm_title = fm.get('title') or fm.get('pageTitle') or fm.get('docid')
res.append({
'path': doc_path,
'title': linked_fm_title,
'docid': docid,
'breadcrumbs': fm.get('breadcrumbs'),
'authors': fm.get('authors'),
'pageKeywords': fm.get('pageKeywords'),
'reasons': [origins[docid]],
'snippet': content[:300] + '...' if content else '',
'linked_info': search_md.check_linked_sections(content, root_input.value)
})
seen_docids.add(docid)
seen_paths.add(doc_path)
return res
new_rows = await anyio.to_thread.run_sync(fetch_linked)
if new_rows:
table.rows.extend(new_rows)
table.rows = list(table.rows)
ui.notify(f'Added {len(new_rows)} linked articles to results', color='positive')
status.set_text(f'Added {len(new_rows)} linked articles. Total rows: {len(table.rows)}')
else:
ui.notify('All linked articles are already in the table', color='info')
status.set_text(f'All linked articles are already in the table. Total rows: {len(table.rows)}')
except Exception as e:
ui.notify(f'Failed to expand links: {e}', color='negative')
status.set_text(f'Expand failed: {e}')
def show_links_modal_handler(args):
with table.client:
path = args.get('path')
section_type = args.get('type')
row = None
for r in table.rows:
if r['path'] == path:
row = r
break
if not row:
return
info = row.get('linked_info', {})
sec_info = info.get(section_type, {})
links = sec_info.get('links', [])
if not links:
ui.notify('No links available to display', color='warning')
return
section_title = 'Anatomy' if section_type == 'anatomy' else 'Differential Diagnosis'
with ui.dialog() as dialog, ui.card().classes('w-[550px] p-6 bg-slate-900 text-white border border-slate-700 rounded-xl gap-4'):
ui.label(f'Linked {section_title}').classes('text-2xl font-bold text-blue-400')
ui.label(f'Source: {row["title"]}').classes('text-slate-400 text-sm')
with ui.column().classes('w-full gap-2 mt-2 max-h-[300px] overflow-y-auto'):
for link in links:
docid = link['docid']
linked_title = link['title']
already_added = any(r.get('docid') == docid for r in table.rows if r.get('docid'))
with ui.row().classes('w-full items-center justify-between p-2 rounded bg-slate-800 border border-slate-700 hover:border-blue-500 transition-all'):
with ui.row().classes('items-center gap-2 flex-grow'):
ui.icon('link', color='primary').classes('text-sm')
ui.link(linked_title, f'/document/{docid}', new_tab=True).classes('text-blue-300 hover:text-blue-200 font-semibold text-sm')
if already_added:
ui.label('Added').classes('text-green-400 text-xs font-semibold px-2 py-1 rounded bg-green-950/40 border border-green-800')
else:
def make_add_handler(d_id=docid, title=linked_title):
return lambda: asyncio.create_task(add_single_doc(d_id, title, row['title'], section_title, dialog))
ui.button(icon='add', on_click=make_add_handler()).classes('text-xs text-blue-400 hover:bg-blue-900/20').props('flat round dense')
with ui.row().classes('w-full justify-between items-center mt-4 pt-4 border-t border-slate-800'):
to_add = [l for l in links if not any(r.get('docid') == l['docid'] for r in table.rows if r.get('docid'))]
if to_add:
def add_all_handler():
return asyncio.create_task(add_all_docs(links, row['title'], section_title, dialog))
ui.button(f'Add All ({len(to_add)})', icon='playlist_add', on_click=add_all_handler, color='primary').classes('text-white font-semibold')
else:
ui.button('All Added', icon='check', color='positive').props('disabled').classes('text-slate-400')
ui.button('Close', on_click=dialog.close).props('outline').classes('text-slate-300 border-slate-700')
dialog.open()
async def add_single_doc(docid: str, title: str, parent_title: str, section_title: str, dialog: ui.dialog):
with table.client:
status.set_text(f'Adding {title}...')
try:
def fetch():
fm, content, doc_path = search_md.get_doc_by_id('docs_md/articles', docid)
if fm:
return {
'path': doc_path,
'title': fm.get('title') or fm.get('pageTitle') or title,
'docid': docid,
'breadcrumbs': fm.get('breadcrumbs'),
'authors': fm.get('authors'),
'pageKeywords': fm.get('pageKeywords'),
'reasons': [f'Linked ({section_title} of {parent_title})'],
'linked_info': search_md.check_linked_sections(content, root_input.value)
}
return None
res = await anyio.to_thread.run_sync(fetch)
if res:
table.rows.append(res)
table.rows = list(table.rows)
ui.notify(f'Added "{title}" to search results', color='positive')
status.set_text(f'Added "{title}". Total rows: {len(table.rows)}')
dialog.close()
else:
ui.notify(f'Could not load document for {title}', color='warning')
except Exception as e:
ui.notify(f'Error adding document: {e}', color='negative')
async def add_all_docs(links: List[Dict[str, str]], parent_title: str, section_title: str, dialog: ui.dialog):
with table.client:
status.set_text('Adding all linked documents...')
try:
seen_docids = {r.get('docid') for r in table.rows if r.get('docid')}
def fetch_all():
res = []
for link in links:
docid = link['docid']
if docid and docid not in seen_docids:
fm, content, doc_path = search_md.get_doc_by_id('docs_md/articles', docid)
if fm:
res.append({
'path': doc_path,
'title': fm.get('title') or fm.get('pageTitle') or link['title'],
'docid': docid,
'breadcrumbs': fm.get('breadcrumbs'),
'authors': fm.get('authors'),
'pageKeywords': fm.get('pageKeywords'),
'reasons': [f'Linked ({section_title} of {parent_title})'],
'linked_info': search_md.check_linked_sections(content, root_input.value)
})
seen_docids.add(docid)
return res
new_rows = await anyio.to_thread.run_sync(fetch_all)
if new_rows:
table.rows.extend(new_rows)
table.rows = list(table.rows)
ui.notify(f'Added {len(new_rows)} linked articles to results', color='positive')
status.set_text(f'Added {len(new_rows)} linked articles. Total rows: {len(table.rows)}')
dialog.close()
else:
ui.notify('No new articles were added', color='info')
except Exception as e:
ui.notify(f'Error adding documents: {e}', color='negative')
async def run_search_handler(): async def run_search_handler():
has_query = False has_query = False
@@ -354,7 +634,8 @@ def search_page() -> None: # build UI
'', '',
'exact', 'exact',
None, None,
or_queries_param or_queries_param,
expand_chk.value
) )
table.rows = results table.rows = results
@@ -540,6 +821,129 @@ def search_page() -> None: # build UI
run_conv_button = ui.button('Run conversion', on_click=lambda: asyncio.create_task(run_dtm_handler()), color='primary') run_conv_button = ui.button('Run conversion', on_click=lambda: asyncio.create_task(run_dtm_handler()), color='primary')
@ui.page('/document/{identifier}')
def render_doc_page(identifier: str):
ui.add_head_html('''
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Outfit', sans-serif;
background-color: #0b0f19;
color: #e2e8f0;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.header-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border: 1px solid #334155;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
}
.markdown-content h1 {
font-size: 2.25rem;
font-weight: 800;
color: #3b82f6;
border-bottom: 2px solid #1e293b;
padding-bottom: 0.5rem;
margin-top: 2rem;
margin-bottom: 1rem;
}
.markdown-content h2 {
font-size: 1.75rem;
font-weight: 700;
color: #60a5fa;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.markdown-content h3 {
font-size: 1.35rem;
font-weight: 600;
color: #93c5fd;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
.markdown-content p, .markdown-content li {
color: #cbd5e1;
font-size: 1.05rem;
}
.markdown-content ul {
list-style-type: disc;
padding-left: 1.5rem;
}
.markdown-content img {
max-width: 100%;
border-radius: 8px;
border: 1px solid #334155;
margin: 1.5rem 0;
display: block;
}
.markdown-content em {
color: #94a3b8;
font-style: italic;
}
.breadcrumb-chip {
background-color: rgba(59, 130, 246, 0.15);
color: #93c5fd;
border: 1px solid rgba(59, 130, 246, 0.3);
padding: 4px 10px;
border-radius: 9999px;
font-size: 0.85rem;
font-weight: 500;
}
</style>
''')
fm, content, path = search_md.get_doc_by_id('docs_md/articles', identifier)
if not fm:
with ui.column().classes('w-full items-center justify-center p-12 gap-4'):
ui.icon('warning', size='4rem', color='negative')
ui.label(f'Document "{identifier}" not found.').classes('text-2xl text-red-400 font-bold')
ui.button('Go to Search', on_click=lambda: ui.navigate.to('/')).props('outline')
return
with ui.column().classes('container gap-6'):
ui.button('Back to Explorer', icon='arrow_back', on_click=lambda: ui.navigate.to('/')).classes('self-start text-blue-400 border border-blue-800').props('outline dense')
with ui.column().classes('header-card w-full gap-4'):
bcs = fm.get('breadcrumbs') or []
if bcs:
with ui.row().classes('gap-2 items-center flex-wrap'):
for b in bcs:
ui.label(b).classes('breadcrumb-chip')
title_text = fm.get('title') or fm.get('pageTitle') or "Untitled"
ui.label(title_text).classes('text-4xl font-extrabold text-white leading-tight')
with ui.row().classes('w-full gap-6 mt-2'):
if fm.get('category'):
with ui.column().classes('gap-0.5'):
ui.label('Category').classes('text-slate-400 text-xs uppercase font-semibold')
ui.label(fm.get('category')).classes('text-slate-200 font-medium')
if fm.get('type'):
with ui.column().classes('gap-0.5'):
ui.label('Type').classes('text-slate-400 text-xs uppercase font-semibold')
ui.label(fm.get('type')).classes('text-slate-200 font-medium')
if fm.get('lastUpdated'):
with ui.column().classes('gap-0.5'):
ui.label('Last Updated').classes('text-slate-400 text-xs uppercase font-semibold')
ui.label(fm.get('lastUpdated')).classes('text-slate-200 font-medium')
authors = fm.get('authors') or []
if authors:
with ui.column().classes('gap-1 mt-2'):
ui.label('Authors').classes('text-slate-400 text-xs uppercase font-semibold')
authors_text = ", ".join([a.get('value') if isinstance(a, dict) else str(a) for a in authors])
ui.label(authors_text).classes('text-blue-300 font-medium')
with ui.card().classes('w-full p-8 shadow-xl bg-slate-900/60 border border-slate-800 rounded-xl markdown-content'):
ui.markdown(content)
def main() -> None: def main() -> None:
port = int(os.environ.get('PORT', '8081')) port = int(os.environ.get('PORT', '8081'))
# NiceGUI's ui.run will serve the app; mount the page at /search-md # NiceGUI's ui.run will serve the app; mount the page at /search-md