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
+189 -2
View File
@@ -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
+412 -8
View File
@@ -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;
}
</style>
''')
@@ -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', '''
<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;">
{{ props.row.title }}
</a>
<span v-else class="text-slate-200 font-semibold">{{ props.row.title }}</span>
<div class="row items-center no-wrap q-gutter-xs">
<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;">
{{ props.row.title }}
</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>
''')
table.add_slot('body-cell-reasons', '''
@@ -315,6 +366,36 @@ def search_page() -> None: # build UI
</span>
</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', '''
<q-td :props="props">
<span class="text-slate-400 text-xs">
@@ -322,6 +403,205 @@ def search_page() -> None: # build UI
</span>
</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():
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('''
<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:
port = int(os.environ.get('PORT', '8081'))
# NiceGUI's ui.run will serve the app; mount the page at /search-md