enhavce search
This commit is contained in:
+189
-2
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user