From dea57ff45f854ec51c8b6d67427db302ee8c4389 Mon Sep 17 00:00:00 2001 From: Ross Date: Sat, 11 Jul 2026 15:19:16 +0100 Subject: [PATCH] enhavce search --- results.json | 640 +++++++---------------------------------- tools/search_md.py | 191 +++++++++++- tools/search_md_gui.py | 420 ++++++++++++++++++++++++++- 3 files changed, 705 insertions(+), 546 deletions(-) diff --git a/results.json b/results.json index f84405a..5b035bf 100644 --- a/results.json +++ b/results.json @@ -1,583 +1,151 @@ [ { - "path": "docs_md/articles/adrenal_082ca43c-db5c-4770-aeed-0c6ea317e8fc.md", - "title": "Adrenal", - "docid": "082ca43c-db5c-4770-aeed-0c6ea317e8fc", + "path": "docs_md/articles/intracranial-hemorrhage_3a9cbed6-aa2c-45a0-88bf-b39f1523ee85.md", + "title": "Intracranial Hemorrhage", + "docid": "3a9cbed6-aa2c-45a0-88bf-b39f1523ee85", "breadcrumbs": [ - "Genitourinary", - "Anatomy", - "Adrenal" + "Pediatrics", + "Differential Diagnosis", + "Brain", + "Intracranial Hemorrhage" ], "authors": [ { - "key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45", - "value": "Siva P. Raman, MD" + "key": "47381de4-c9fd-4999-8dd0-1808cd72db6b", + "value": "Luke L. Linscott, MD" } ], - "pageKeywords": "Genitourinary, Anatomy, Adrenal", + "pageKeywords": "Pediatrics, Differential Diagnosis, Brain, Intracranial Hemorrhage", "reasons": [ "Content" ], - "snippet": "...hrine) into bloodstream - **Vessels**,**nerves**, and **lymphatics** - Arteries - **Superior adrenal arteries**: Typic..." - }, - { - "path": "docs_md/articles/sensorineural-hearing-loss-in-child_08c895da-f2aa-4076-abf0-af9aca1677cd.md", - "title": "Sensorineural Hearing Loss in Child", - "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" + "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": { + "exists": false, + "links": [] }, - { - "key": "e987d3d3-1206-48d6-824b-3347c2968855", - "value": "Michael P. Federle, MD, FACR" + "differential": { + "exists": false, + "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", - "title": "Normal-Pressure Hydrocephalus", - "docid": "ba3f857d-58de-4f21-8463-1631b4cb9972", + "path": "docs_md/articles/epilepsy-child_a342e5b5-5b98-4003-a437-6d42a483b40e.md", + "title": "Epilepsy, Child", + "docid": "a342e5b5-5b98-4003-a437-6d42a483b40e", "breadcrumbs": [ "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", - "CPA-IAC and Posterior Fossa", + "Brain Parenchyma, General", "Clinically Based Differentials", - "Sensorineural Hearing Loss in Adult" + "Epilepsy, Child" ], "authors": [ { - "key": "eef2f839-5706-47b9-89c3-60d8315b2b3a", - "value": "Nicholas A. Koontz, MD" + "key": "47381de4-c9fd-4999-8dd0-1808cd72db6b", + "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": [ "Content" ], - "snippet": "...ove; fat saturation suppresses fat signal - **Large Endolymphatic Sac Anomaly (IP-2)** - Most common lesion found in children with bilat..." - }, - { - "path": "docs_md/articles/abdominal-lymph-nodes_3ca98d42-20ef-48fe-9265-ebfe570ba54b.md", - "title": "Abdominal Lymph Nodes", - "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" + "snippet": "...tes without recurrence does not require imaging - Trauma, remote stroke, or infection results in encephalomalacia &/or gliosis, which may ca...", + "linked_info": { + "anatomy": { + "exists": false, + "links": [] }, - { - "key": "33151213-01b2-4542-9105-342e006b3915", - "value": "H. Ric Harnsberger, MD" + "differential": { + "exists": false, + "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", - "title": "Colonic Wall Thickening", - "docid": "71ca9b30-6186-4129-957b-3b686b91b327", - "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", + "path": "docs_md/articles/abusive-head-trauma_c57982e3-fa8f-4fd6-9184-04fd1d37a906.md", + "title": "Abusive Head Trauma", + "docid": "c57982e3-fa8f-4fd6-9184-04fd1d37a906", "breadcrumbs": [ "Pediatrics", "Diagnosis", "Pediatric Neuroradiology", "Brain", - "Anatomy-Based Diagnoses", - "Ventricles and Cisterns", - "Hydrocephalus" + "Pathology-Based Diagnoses", + "Trauma", + "Abusive Head Trauma" ], "authors": [ { - "key": "838e1722-2479-4fbd-a5fe-d965980a1a2c", - "value": "Blaise V. Jones, MD" + "key": "47381de4-c9fd-4999-8dd0-1808cd72db6b", + "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": [ - "Content" + "Breadcrumbs", + "Content", + "Keywords", + "Title" ], - "snippet": "...Extracellular, extravascular fluid in brain is managed by glymphatic system - CSF circulates from within ventricles → subarachnoid space →..." - }, - { - "path": "docs_md/articles/inner-ear-lesion-adult_e5bbf757-d77a-4546-a848-d1a1a64cb230.md", - "title": "Inner Ear Lesion, Adult", - "docid": "e5bbf757-d77a-4546-a848-d1a1a64cb230", - "breadcrumbs": [ - "Head and Neck", - "Differential Diagnosis", - "Temporal Bone", - "Anatomically Based Differentials", - "Inner Ear Lesion, Adult" - ], - "authors": [ - { - "key": "d19354f3-7ff2-495a-ad3f-064122e45602", - "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" + "snippet": "# KEY FACTS - ## Terminology - Abusive head trauma (AHT) - Traumatic injury inflicted on infants & children by adults...", + "linked_info": { + "anatomy": { + "exists": true, + "links": [ + { + "title": "Visual Network", + "docid": "404625d9-3125-4923-9f9d-53d0f81c3542" + }, + { + "title": "Anatomy Document", + "docid": "bf20450d-2629-4795-98e7-7788b665ad3c" + }, + { + "title": "Anatomy Document", + "docid": "8fb2f541-799d-479f-b000-ab54f216199a" + } + ] }, - { - "key": "3d1e4c57-c1cf-4c89-b0f0-5d82b29a31e1", - "value": "Suhny Abbara, MD, FACR, MSCCT, FNASCI" + "differential": { + "exists": true, + "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..." + } } ] \ No newline at end of file diff --git a/tools/search_md.py b/tools/search_md.py index 2630bc6..d56115e 100644 --- a/tools/search_md.py +++ b/tools/search_md.py @@ -20,7 +20,8 @@ import argparse import csv import json import os -from typing import List, Dict, Any +import re +from typing import List, Dict, Any, Tuple, Optional import yaml import fnmatch @@ -249,7 +250,135 @@ def match_author(authors: List[Dict[str, Any]], q: str) -> bool: 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 = [] # 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'), 'reasons': sorted(list(set(reasons))), '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 diff --git a/tools/search_md_gui.py b/tools/search_md_gui.py index 7f4eb55..648afd4 100644 --- a/tools/search_md_gui.py +++ b/tools/search_md_gui.py @@ -18,7 +18,11 @@ from typing import List, Dict, Any import anyio 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 subprocess @@ -156,6 +160,44 @@ def search_page() -> None: # build UI color: #cbd5e1; 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; + } ''') @@ -243,7 +285,9 @@ def search_page() -> None: # build UI render_query_blocks() 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') # Advanced Criteria Expansion Box @@ -280,7 +324,8 @@ def search_page() -> None: # build UI columns = [ {'name': 'title', 'label': 'Title', 'field': 'title', 'required': True, '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': '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 table.add_slot('body-cell-title', ''' - - {{ props.row.title }} - - {{ props.row.title }} +
+ + {{ props.row.title }} + + + Expand Linked Articles + +
''') table.add_slot('body-cell-reasons', ''' @@ -315,6 +366,36 @@ def search_page() -> None: # build UI ''') + table.add_slot('body-cell-anatomy', ''' + + + No Anatomy ❌ + + + Anatomy ({{ props.row.linked_info.anatomy.links.length }}) 🔍 + + + Anatomy (0) + + + ''') + table.add_slot('body-cell-differential', ''' + + + No Diff Diag ❌ + + + Diff Diag ({{ props.row.linked_info.differential.links.length }}) 🔍 + + + Diff Diag (0) + + + ''') table.add_slot('body-cell-path', ''' @@ -322,6 +403,205 @@ def search_page() -> None: # build UI ''') + 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(): has_query = False @@ -354,7 +634,8 @@ def search_page() -> None: # build UI '', 'exact', None, - or_queries_param + or_queries_param, + expand_chk.value ) 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') +@ui.page('/document/{identifier}') +def render_doc_page(identifier: str): + ui.add_head_html(''' + + + ''') + + 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: port = int(os.environ.get('PORT', '8081')) # NiceGUI's ui.run will serve the app; mount the page at /search-md