Wire up query history: localStorage session persistence + sidebar UI

This commit is contained in:
2026-08-14 17:44:36 +07:00
parent 9be5819710
commit 057d4ed9dc
23 changed files with 1231 additions and 30 deletions
+53 -4
View File
@@ -147,12 +147,27 @@ SECTION_PHRASES: dict[str, tuple[str, ...]] = {
}
# Book order of monograph sections (Hướng dẫn sử dụng, printed page 39). Used to
# present a whole-drug overview when the query names the drug but no attribute —
# typing "PARACETAMOL" should return the monograph, never a "specify an
# attribute" dead-end.
# Book order of monograph sections, verified 2026-08-14 against the actual
# PDF (physical page 38 = printed page 39, "HƯỚNG DẪN SỬ DỤNG DƯỢC THƯ QUỐC
# GIA VIỆT NAM"), not assumed from an earlier reading of this constant. The
# guide numbers 19 items; item 1, "Tên chuyên luận thuốc", is the monograph's
# own title/heading, not a content section with a `section_key` — items 2-19
# are exactly these 18 keys, in exactly this order. Confirms this tuple was
# already complete and correctly ordered for the book's own stated template.
#
# `ten_thuong_mai` (trade name) is real, present in the corpus (492/684
# drugs) but is NOT one of the guide's 19 numbered items — the book's own
# template never promises it, so there is no book-verified position to place
# it at. Inserted right after `ten_chung_quoc_te` (generic/INN name) as the
# most natural adjacency (same convention `understanding.py`'s `SECTION_KEYS`
# already uses) — a judgment call, not a sourced fact, unlike the 18 above.
#
# Used to present a whole-drug overview when the query names the drug but no
# attribute — typing "PARACETAMOL" should return the monograph, never a
# "specify an attribute" dead-end.
SECTION_ORDER: tuple[str, ...] = (
"ten_chung_quoc_te",
"ten_thuong_mai",
"ma_atc",
"loai_thuoc",
"dang_thuoc_va_ham_luong",
@@ -208,3 +223,37 @@ class SectionResolver:
if f" {normalized_phrase} " in padded:
return SectionMatch(section_key, phrase)
return None
def resolve_all(self, query: str) -> tuple[SectionMatch, ...]:
"""Every distinct section a question genuinely names, not just the
first. Same longest-first order as `resolve()`, and the same
span-claiming rule: once a phrase's occurrence is accepted, any
shorter phrase whose only occurrence falls inside that already-
claimed span is a substring of it, not a second section — e.g.
"chỉ định" inside "chống chỉ định của X" must NOT count as a second,
separate mention of `chi_dinh`. A phrase counts only when it has an
occurrence that does not overlap any span already claimed by a
longer, earlier-accepted phrase. Returns `()` for no match and
exactly one item when the question names only one section — this is
a superset of `resolve()`, not a replacement for it.
"""
normalized_query = normalize_name(query)
if not normalized_query:
return ()
padded = f" {normalized_query} "
claimed: list[tuple[int, int]] = []
seen_sections: set[str] = set()
matches: list[SectionMatch] = []
for normalized_phrase, section_key, phrase in self._index:
needle = f" {normalized_phrase} "
idx = padded.find(needle)
if idx == -1:
continue
span = (idx, idx + len(needle))
if any(span[0] < c_end and c_start < span[1] for c_start, c_end in claimed):
continue
claimed.append(span)
if section_key not in seen_sections:
seen_sections.add(section_key)
matches.append(SectionMatch(section_key, phrase))
return tuple(matches)