Spaces:
Running
Running
Commit ·
9b44273
1
Parent(s): 2f3b92c
Expand POPULAR_TERMS: 40 -> 293 via 84000.org dictionary
Browse files- tipatika-wiki/wiki/concepts/popular-terms-seed.md +46 -0
- tipatika-wiki/wiki/log.md +29 -0
- webapp/tipitaka-api/app/database/sqlite_db.py +26 -0
- webapp/tipitaka-api/app/main.py +2 -0
- webapp/tipitaka-api/app/routers/search.py +10 -0
- webapp/tipitaka-api/app/services/search_service.py +149 -1
- webapp/tipitaka-web/src/components/layout/NavDrawer.tsx +100 -3
- webapp/tipitaka-web/src/stores/searchStore.ts +24 -3
tipatika-wiki/wiki/concepts/popular-terms-seed.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
type: concept
|
| 3 |
+
title: POPULAR_TERMS — คำศัพท์ตั้งต้นสำหรับ Autocomplete
|
| 4 |
+
tags: [search, seed-data, buddhism, dictionary]
|
| 5 |
+
related: [search-autocomplete, search-pipeline]
|
| 6 |
+
created: 2026-04-30
|
| 7 |
+
updated: 2026-04-30
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# POPULAR_TERMS — คำศัพท์ตั้งต้น
|
| 11 |
+
|
| 12 |
+
## Description
|
| 13 |
+
|
| 14 |
+
List **293** คำศัพท์พระไตรปิฎกที่ curate ไว้ใน `search_service.py` เป็น fallback เมื่อ `search_log` ยังว่างหรือ match ไม่ครบ
|
| 15 |
+
|
| 16 |
+
## Location
|
| 17 |
+
|
| 18 |
+
`webapp/tipitaka-api/app/services/search_service.py` → `POPULAR_TERMS: List[str]`
|
| 19 |
+
|
| 20 |
+
## Source
|
| 21 |
+
|
| 22 |
+
| Source | Count |
|
| 23 |
+
|--------|-------|
|
| 24 |
+
| Core curated terms (original v1) | 40 |
|
| 25 |
+
| 84000.org dictionary topic headings (v2 expansion) | 253 |
|
| 26 |
+
| **Total** | **293** |
|
| 27 |
+
|
| 28 |
+
คำศัพท์ 253 คำใหม่มาจากหัวข้อ (topic headings) ของ [พจนานุกรมพุทธศาสตร์ ฉบับประมวลธรรม](https://84000.org/tipitaka/dic/) โดยตัดเลขหมวด (varga numbers) ออก ทำ unique และ filter กับรายการเดิม
|
| 29 |
+
|
| 30 |
+
## รายการปัจจุบัน (2026-04-30)
|
| 31 |
+
|
| 32 |
+
### หมวด 1 — หลักธรรมพื้นฐาน (40 คำ — original)
|
| 33 |
+
|
| 34 |
+
อริยสัจ, นิพพาน, ไตรลักษณ์, อนิจจัง, ทุกข์, อนัตตา, สติปัฏฐาน, อัฏฐมรรค, โพชฌงค์, ปฏิจจสมุปบาท, สมาธิ, วิปัสสนา, สมถะ, ศีล, ปัญญา, กรรม, วิบาก, กิเลส, ตัณหา, อวิชชา, โพธิสัตว์, ปรินิพพาน, สังสารวัฏ, มรนสติ, เมตตา, กรุณา, มุทิตา, อุเบกขา, อภิธรรม, วินัยปิฎก, สุตตันตปิฎก, ขันธ์, อายตนะ, ธาตุ, อินทรีย์, โพธิปักขิยธรรม, สัมมาสมาธิ, สัมมาสติ, บารมี, เจตสิก, จิต, รูป, นาม
|
| 35 |
+
|
| 36 |
+
### หมวด 2 — จาก 84000.org dictionary (253 คำ — v2 expansion)
|
| 37 |
+
|
| 38 |
+
กถาวัตถุ, กรรมกิเลส, กรรมฐาน, กสิณ, กัลยาณมิตตตา, กัลยาณมิตรธรรม, กาม, กามคุณ, กามโภคี, กาลามสูตรกังขานิยฐาน, กิจ, กิจในอริยสัจจ์, กุลจิรัฏฐิติธรรม, กุศลกรรมบถ, กุศลมูล, กุศลวิตก, คารวะ, ฆราวาสธรรม, จรณะ, จริต, จักขุ, จักร, จักรวรรดิวัตร, ฌาน, ฌาน 2 ประเภท, ญาณ, ถูปารหบุคคล, ทวาร, ทศพลญาณ, ทักขิณาวิสุทธิ, ทาน, ทิฏฐธัมมิกัตถสังวัตตนิกธรรม, ทิฏฐิ, ทิศ, ที่สุด, ทุกขตา, ทุจริต, ธรรม, ธรรมขันธ์, ธรรมคุณ, ธรรมคุ้มครองโลก, ธรรมทำให้งาม, ธรรมนิยาม, ธรรมมีอุปการะมาก, ธรรมสมาทาน, ธรรมสมาธิ, ธรรมสวนานิสงส์, ธรรมเทสกธรรม, ธาตุกัมมัฏฐาน, ธุดงค์, ธุระ, นวกภิกขุธรรม, นวังคสัตถุสาสน์, นาถกรณธรรม, นิมิต, นิยาม, นิวรณ์, นิโรธ, บริษัท, บุคคล, บุคคลหาได้ยาก, บุญกิริยาวัตถุ, บุตร, บุพนิมิตแห่งมรรค, บูชา, ปฏิปทา, ปฏิสันถาร, ปฏิสัมภิทา, ปธาน, ปปัญจะ, ปปัญจธรรม, ปรมัตถธรรม, ประมาณ, ปริญญา, ปริเยสนา, ปัจจัย, ปัจจัยให้เกิดสัมมาทิฏฐิ, ปัพพชิตอภิณหปัจจเวกขณ์, ปาฏิหาริย์, ปาปณิกธรรม, ปาพจน์, ปาริสุทธิศีล, ปิยรูป สาตรูป, ปีติ, ผล, พร, พรหมวิหาร, พละ, พละ 5 ของพระมหากษัตริย์, พหูสูตมีองค์, พุทธคุณ, พุทธจริยา, พุทธโอวาท, ภพ, ภรรยา, ภัพพ���าธรรม, ภาวนา, ภูมิ, มงคล, มรรค, มรรคมีองค์, มละ, มหาปเทส, มหาปเทส 4 เฉพาะในทางพระวินัย, มหาภูต, มัจฉริยะ, มานะ, มาร, มิจฉัตตะ, มิตรปฏิรูปก์, รัตนตรัย, ราชธรรม, ฤทธิ์, ลักษณะตัดสินธรรมวินัย, ลัทธินอกพระพุทธศาสนา, ลีลาการสอน, วณิชชา, วรรณะ, วัฏฏะ, วัฒนมุข, วัตถุประสงค์ในการบัญญัติวินัย, วิชชา, วิญญาณ, วิญญาณฐิติ, วิธีปฏิบัติต่อทุกข์-สุข, วิบัติ, วิปัลลาส, วิปัสสนาญาณ, วิปัสสนูปกิเลส, วิมุตติ, วิรัติ, วิสุทธิ, วิเวก, วิโมกข์, วุฒิ, ศรัทธา, ศีล 8 ทั้งอาชีวะ, สกทาคามี, สมชีวิธรรม, สมบัติ, สมาธิภาวนา, สมาบัติ, สรณะ, สวรรค์, สังขตลักษณะ, สังขาร, สังคหวัตถุ, สังคหวัตถุของผู้ครองแผ่นดิน, สังคหะ, สังฆคุณ, สังวร, สังเวชนียสถาน, สังโยชน์, สัจจะ, สัญญา, สัตตาวาส, สัทธรรม, สันโดษ, สัปปายะ, สัปปุริสทาน, สัปปุริสธรรม, สัปปุริสบัญญัติ, สัมปชัญญะ, สัมปทา, สัมปรายิกัตถสังวัตตนิกธรรม, สัมผัส, สัมมัตตะ, สารณียธรรม, สาสน์, สิกขา, สุข, สุขของคฤหัสถ์, สุจริต, สุทธาวาส, สุทธิ, สุหทมิตร, อกุศลกรรมบถ, อกุศลมูล, อกุศลวิตก, อคติ, อธิปไตย, อธิษฐาน, อนันตริยกรรม, อนาคามี, อนุตตริยะ, อนุบุพพวิหาร, อนุปุพพิกถา, อนุสติ, อนุสัย, อบาย, อบายมุข, อปริหานิยธรรม, อปัณณกปฏิปทา, อปัสเสนะ, อภิญญา, อภิฐาน, อภิณหปัจจเวกขณ์, อภิสังขาร, อรหันต์, อริยทรัพย์, อริยบุคคล, อริยวงค์, อริยวัฑฒิ, อริยสัจจ์, อรูป, อสังขตลักษณะ, อสุภะ, อัคคิ, อัตถะ, อันตคาหิกทิฏฐิ, อันตรายของภิกษุสามเณรผู้บวชใหม่, อัปปมาทะ, อาการที่พระพุทธเจ้าทรงสั่งสอน, อาจารย์, อานาปานสติ 16 ฐาน, อายตนะภายนอก, อายตนะภายใน, อายุสสธรรม, อาวาสิกธรรม, อาสวะ, อาหาร, อิทธิบาท, อุบาสกธรรม, อุปกิเลส, อุปัญญาตธรรม, อุปาทาน, อุปาทารูป, เจดีย์, เจตนา, เถรธรรม, เทพ, เทวทูต, เทศนา, เบญจธรรม, เมถุนสังโยค, เวทนา, เวปุลลธรรม, เวปุลละ, เวสารัชชกรณธรรม, เวสารัชชะ, โกศล, โภควิภาค, โภคอาทิยะ, โยคะ, โยนิ, โยนิโสมนสิการ, โลก, โลกธรรม, โลกุตตรธรรม, โสดาบัน, โสตาปัตติยังคะ, โอฆะ, ไตรปิฎก
|
| 39 |
+
|
| 40 |
+
## หลักเกณฑ์การเพิ่ม
|
| 41 |
+
|
| 42 |
+
1. **ที่มาเด่น**: คำศัพท์จาก [84000.org/tipitaka/dic/](https://84000.org/tipitaka/dic/) — หัวข้อหมวดธรรม (topic headings) โดยตัดเลขหมวดออก
|
| 43 |
+
2. **ไม่มีเลขหมวด**: ตัดตัวเลขท้าย เช่น "กรรม 2" → "กรรม", "กาม 2" → "กาม"
|
| 44 |
+
3. **ไม่มี cross-reference**: ตัดตัวเลขวงเล็บ เช่น "ธรรมมีอุปการะมาก 7 [292]" → "ธรรมมีอ���ปการะมาก"
|
| 45 |
+
4. **deduplicate**: คำที่มีอยู่แล้วใน POPULAR_TERMS ไม่เพิ่มซ้ำ
|
| 46 |
+
5. **ควรมีผลลัพธ์ใน DB**: คำที่ seed ควรตรงกับข้อมูลใน tipitaka_mcu.db จริง
|
tipatika-wiki/wiki/log.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research Log
|
| 2 |
+
|
| 3 |
+
## 2026-04-30
|
| 4 |
+
|
| 5 |
+
- Project created
|
| 6 |
+
- Initialized `purpose.md` — defined project scope: RAG-based Q&A system for Pali Tipitaka, covering architecture, themes, milestones, and vision
|
| 7 |
+
- Corrected `purpose.md` — repo URL changed from GitHub to Hugging Face Spaces (https://huggingface.co/spaces/dhammawatthumpra/tipitaka)
|
| 8 |
+
|
| 9 |
+
## 2026-04-30 (ingest | Search Autocomplete Feature)
|
| 10 |
+
|
| 11 |
+
Pages written: [[search-autocomplete]], [[popular-terms-seed]], [[search-pipeline]]
|
| 12 |
+
Pages updated: index.md, overview.md
|
| 13 |
+
|
| 14 |
+
Files changed in codebase:
|
| 15 |
+
- `sqlite_db.py` — get_disk_connection(), ensure_search_log_table()
|
| 16 |
+
- `search_service.py` — POPULAR_TERMS, get_suggestions(), log_search(), search() restructured
|
| 17 |
+
- `routers/search.py` — GET /api/search/suggestions endpoint
|
| 18 |
+
- `main.py` — ensure_search_log_table() on startup
|
| 19 |
+
- `searchStore.ts` — suggestions state + fetchSuggestions()
|
| 20 |
+
- `NavDrawer.tsx` — debounce, dropdown UI, keyboard nav
|
| 21 |
+
|
| 22 |
+
## 2026-04-30 (expand | 84000 Dictionary → POPULAR_TERMS)
|
| 23 |
+
|
| 24 |
+
Pages updated: [[popular-terms-seed]]
|
| 25 |
+
|
| 26 |
+
Summary: Scraped 359 topic headings from [พจนานุกรมพุทธศาสตร์ ฉบับประมวลธรรม](https://84000.org/tipitaka/dic/), stripped varga numbers, deduplicated, and merged with existing POPULAR_TERMS (40 terms → 293 terms total).
|
| 27 |
+
|
| 28 |
+
Files changed in codebase:
|
| 29 |
+
- `search_service.py` — POPULAR_TERMS expanded from 40 → 293 terms (253 new from dictionary)
|
webapp/tipitaka-api/app/database/sqlite_db.py
CHANGED
|
@@ -56,6 +56,32 @@ class SQLiteDB:
|
|
| 56 |
|
| 57 |
self._mem_conn = mem
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
@property
|
| 60 |
def is_in_memory(self) -> bool:
|
| 61 |
return self._mem_conn is not None
|
|
|
|
| 56 |
|
| 57 |
self._mem_conn = mem
|
| 58 |
|
| 59 |
+
@contextmanager
|
| 60 |
+
def get_disk_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
| 61 |
+
"""Always connects to disk — used for persistent writes (e.g. search_log)."""
|
| 62 |
+
conn = sqlite3.connect(self.db_path)
|
| 63 |
+
conn.row_factory = sqlite3.Row
|
| 64 |
+
try:
|
| 65 |
+
yield conn
|
| 66 |
+
conn.commit()
|
| 67 |
+
finally:
|
| 68 |
+
conn.close()
|
| 69 |
+
|
| 70 |
+
def ensure_search_log_table(self) -> None:
|
| 71 |
+
"""Create search_log table on disk if not exists."""
|
| 72 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 73 |
+
conn.execute("""
|
| 74 |
+
CREATE TABLE IF NOT EXISTS search_log (
|
| 75 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 76 |
+
query TEXT NOT NULL COLLATE NOCASE,
|
| 77 |
+
count INTEGER NOT NULL DEFAULT 1,
|
| 78 |
+
last_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
| 79 |
+
UNIQUE(query)
|
| 80 |
+
)
|
| 81 |
+
""")
|
| 82 |
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_search_log_query ON search_log(query)")
|
| 83 |
+
conn.commit()
|
| 84 |
+
|
| 85 |
@property
|
| 86 |
def is_in_memory(self) -> bool:
|
| 87 |
return self._mem_conn is not None
|
webapp/tipitaka-api/app/main.py
CHANGED
|
@@ -21,6 +21,8 @@ async def lifespan(app: FastAPI):
|
|
| 21 |
logger.info("Initializing database...")
|
| 22 |
db.load_to_memory()
|
| 23 |
logger.info("Database loaded to memory — all queries now run in-memory.")
|
|
|
|
|
|
|
| 24 |
|
| 25 |
yield
|
| 26 |
# Shutdown logic (if any)
|
|
|
|
| 21 |
logger.info("Initializing database...")
|
| 22 |
db.load_to_memory()
|
| 23 |
logger.info("Database loaded to memory — all queries now run in-memory.")
|
| 24 |
+
db.ensure_search_log_table()
|
| 25 |
+
logger.info("Search log table ready.")
|
| 26 |
|
| 27 |
yield
|
| 28 |
# Shutdown logic (if any)
|
webapp/tipitaka-api/app/routers/search.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from fastapi import APIRouter, Depends, Query
|
|
|
|
| 2 |
from app.schemas import SearchResponse
|
| 3 |
from app.services.search_service import SearchService
|
| 4 |
from app.database.sqlite_db import get_db, SQLiteDB
|
|
@@ -8,6 +9,15 @@ router = APIRouter(prefix="/search", tags=["Search"])
|
|
| 8 |
def get_search_service(db: SQLiteDB = Depends(get_db)) -> SearchService:
|
| 9 |
return SearchService(db)
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
@router.get("", response_model=SearchResponse)
|
| 12 |
def search(
|
| 13 |
q: str = Query(..., min_length=1),
|
|
|
|
| 1 |
from fastapi import APIRouter, Depends, Query
|
| 2 |
+
from typing import List
|
| 3 |
from app.schemas import SearchResponse
|
| 4 |
from app.services.search_service import SearchService
|
| 5 |
from app.database.sqlite_db import get_db, SQLiteDB
|
|
|
|
| 9 |
def get_search_service(db: SQLiteDB = Depends(get_db)) -> SearchService:
|
| 10 |
return SearchService(db)
|
| 11 |
|
| 12 |
+
@router.get("/suggestions", response_model=List[str])
|
| 13 |
+
def suggestions(
|
| 14 |
+
q: str = Query(..., min_length=1),
|
| 15 |
+
limit: int = Query(8, ge=1, le=20),
|
| 16 |
+
service: SearchService = Depends(get_search_service)
|
| 17 |
+
):
|
| 18 |
+
"""Autocomplete suggestions based on search history + curated terms."""
|
| 19 |
+
return service.get_suggestions(q, limit)
|
| 20 |
+
|
| 21 |
@router.get("", response_model=SearchResponse)
|
| 22 |
def search(
|
| 23 |
q: str = Query(..., min_length=1),
|
webapp/tipitaka-api/app/services/search_service.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import time
|
| 2 |
import re
|
|
|
|
| 3 |
from app.database.sqlite_db import SQLiteDB
|
| 4 |
from app.schemas import SearchResponse, SearchResultItem
|
| 5 |
|
|
@@ -55,10 +56,153 @@ def to_thai_digits(s: str) -> str:
|
|
| 55 |
return s.translate(str.maketrans("0123456789", "๐๑๒๓๔๕๖๗๘๙"))
|
| 56 |
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
class SearchService:
|
| 59 |
def __init__(self, db: SQLiteDB):
|
| 60 |
self.db = db
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def clean_text(self, text: str) -> str:
|
| 63 |
"""Port of v2.1 _clean(): strips line numbers, footnotes, separators."""
|
| 64 |
if not text:
|
|
@@ -210,10 +354,14 @@ class SearchService:
|
|
| 210 |
|
| 211 |
time_ms = (time.time() - start_time) * 1000
|
| 212 |
|
| 213 |
-
|
| 214 |
query=corrected_q,
|
| 215 |
total_results=total_count,
|
| 216 |
results=paginated,
|
| 217 |
time_taken_ms=time_ms,
|
| 218 |
breakdown_by_volume=breakdown_data,
|
| 219 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import time
|
| 2 |
import re
|
| 3 |
+
from typing import List
|
| 4 |
from app.database.sqlite_db import SQLiteDB
|
| 5 |
from app.schemas import SearchResponse, SearchResultItem
|
| 6 |
|
|
|
|
| 56 |
return s.translate(str.maketrans("0123456789", "๐๑๒๓๔๕๖๗๘๙"))
|
| 57 |
|
| 58 |
|
| 59 |
+
# ── Curated popular terms (fallback when search_log is empty) ──────
|
| 60 |
+
# Source: https://84000.org/tipitaka/dic/ (topic headings, stripped of varga numbers)
|
| 61 |
+
POPULAR_TERMS: List[str] = [
|
| 62 |
+
# ── Core doctrines ──────────────────────────────────────────
|
| 63 |
+
"อริยสัจ", "นิพพาน", "ไตรลักษณ์", "อนิจจัง", "ทุกข์", "อนัตตา",
|
| 64 |
+
"สติปัฏฐาน", "อัฏฐมรรค", "โพชฌงค์", "ปฏิจจสมุปบาท",
|
| 65 |
+
"สมาธิ", "วิปัสสนา", "สมถะ", "ศีล", "ปัญญา",
|
| 66 |
+
"กรรม", "วิบาก", "กิเลส", "ตัณหา", "อวิชชา",
|
| 67 |
+
"โพธิสัตว์", "ปรินิพพาน", "สังสารวัฏ", "มรนสติ",
|
| 68 |
+
"เมตตา", "กรุณา", "มุทิตา", "อุเบกขา",
|
| 69 |
+
"อภิธรรม", "วินัยปิฎก", "สุตตันตปิฎก",
|
| 70 |
+
"ขันธ์", "อายตนะ", "ธาตุ", "อินทรีย์",
|
| 71 |
+
"โพธิปักขิยธรรม", "สัมมาสมาธิ", "สัมมาสติ",
|
| 72 |
+
"บารมี", "เจตสิก", "จิต", "รูป", "นาม",
|
| 73 |
+
# ── From 84000 dictionary ───────────────────────────────────
|
| 74 |
+
"กถาวัตถุ", "กรรมกิเลส", "กรรมฐาน", "กสิณ",
|
| 75 |
+
"กัลยาณมิตตตา", "กัลยาณมิตรธรรม", "กาม", "กามคุณ",
|
| 76 |
+
"กามโภคี", "กาลามสูตรกังขานิยฐาน", "กิจ", "กิจในอริยสัจจ์",
|
| 77 |
+
"กุลจิรัฏฐิติธรรม", "กุศลกรรมบถ", "กุศลมูล", "กุศลวิตก",
|
| 78 |
+
"คารวะ", "ฆราวาสธรรม", "จรณะ", "จริต", "จักขุ",
|
| 79 |
+
"จักร", "จักรวรรดิวัตร", "ฌาน", "ฌาน 2 ประเภท", "ญาณ",
|
| 80 |
+
"ถูปารหบุคคล", "ทวาร", "ทศพลญาณ", "ทักขิณาวิสุทธิ",
|
| 81 |
+
"ทาน", "ทิฏฐธัมมิกัตถสังวัตตนิกธรรม", "ทิฏฐิ", "ทิศ",
|
| 82 |
+
"ที่สุด", "ทุกขตา", "ทุจริต", "ธรรม", "ธรรมขันธ์",
|
| 83 |
+
"ธรรมคุณ", "ธรรมคุ้มครองโลก", "ธรรมทำให้งาม", "ธรรมนิยาม",
|
| 84 |
+
"ธรรมมีอุปการะมาก", "ธรรมสมาทาน", "ธรรมสมาธิ",
|
| 85 |
+
"ธรรมสวนานิสงส์", "ธรรมเทสกธรรม", "ธาตุกัมมัฏฐาน",
|
| 86 |
+
"ธุดงค์", "ธุระ", "นวกภิกขุธรรม", "นวังคสัตถุสาสน์",
|
| 87 |
+
"นาถกรณธรรม", "นิมิต", "นิยาม", "นิวรณ์", "นิโรธ",
|
| 88 |
+
"บริษัท", "บัญญัติ 2", "บุคคล", "บุคคลหาได้ยาก",
|
| 89 |
+
"บุญกิริยาวัตถุ", "บุตร", "บุพนิมิตแห่งมรรค", "บูชา",
|
| 90 |
+
"ปฏิปทา", "ปฏิสันถาร", "ปฏิสัมภิทา", "ปธาน",
|
| 91 |
+
"ปปัญจะ, ปปัญจธรรม", "ปรมัตถธรรม", "ประมาณ", "ปริญญา",
|
| 92 |
+
"ปริเยสนา", "ปัจจัย", "ปัจจัยให้เกิดสัมมาทิฏฐิ",
|
| 93 |
+
"ปัพพชิตอภิณหปัจจเวกขณ์", "ปาฏิหาริย์", "ปาปณิกธรรม",
|
| 94 |
+
"ปาพจน์", "ปาริสุทธิศีล", "ปิยรูป สาตรูป", "ปีติ",
|
| 95 |
+
"ผล", "พร", "พรหมวิหาร", "พละ",
|
| 96 |
+
"พละ 5 ของพระมหากษัตริย์", "พหูสูตมีองค์", "พุทธคุณ",
|
| 97 |
+
"พุทธจริยา", "พุทธโอวาท", "ภพ", "ภรรยา",
|
| 98 |
+
"ภัพพตาธรรม", "ภาวนา", "ภูมิ", "มงคล", "มรรค",
|
| 99 |
+
"มรรคมีองค์", "มละ", "มหาปเทส",
|
| 100 |
+
"มหาปเทส 4 เฉพาะในท���งพระวินัย", "มหาภูต", "มัจฉริยะ",
|
| 101 |
+
"มานะ", "มาร", "มิจฉัตตะ", "มิตรปฏิรูปก์",
|
| 102 |
+
"รัตนตรัย", "ราชธรรม", "ฤทธิ์", "ลักษณะตัดสินธรรมวินัย",
|
| 103 |
+
"ลัทธินอกพระพุทธศาสนา", "ลีลาการสอน", "วณิชชา", "วรรณะ",
|
| 104 |
+
"วัฏฏะ", "วัฒนมุข", "วัตถุประสงค์ในการบัญญัติวินัย",
|
| 105 |
+
"วิชชา", "วิญญาณ", "วิญญาณฐิติ",
|
| 106 |
+
"วิธีปฏิบัติต่อทุกข์-สุข", "วิบัติ", "วิปัลลาส",
|
| 107 |
+
"วิปัสสนาญาณ", "วิปัสสนูปกิเลส", "วิมุตติ", "วิรัติ",
|
| 108 |
+
"วิสุทธิ", "วิเวก", "วิโมกข์", "วุฒิ", "ศรัทธา",
|
| 109 |
+
"ศีล 8 ทั้งอาชีวะ", "สกทาคามี", "สมชีวิธรรม", "สมบัติ",
|
| 110 |
+
"สมาธิภาวนา", "สมาบัติ", "สรณะ", "สวรรค์",
|
| 111 |
+
"สังขตลักษณะ", "สังขาร", "สังคหวัตถุ",
|
| 112 |
+
"สังคหวัตถุของผู้ครองแผ่นดิน", "สังคหะ", "สังฆคุณ",
|
| 113 |
+
"สังวร", "สังเวชนียสถาน", "สังโยชน์", "สัจจะ", "สัญญา",
|
| 114 |
+
"สัตตาวาส", "สัทธรรม", "สันโดษ", "สัปปายะ",
|
| 115 |
+
"สัปปุริสทาน", "สัปปุริสธรรม", "สัปปุริสบัญญัติ",
|
| 116 |
+
"สัมปชัญญะ", "สัมปทา", "สัมปรายิกัตถสังวัตตนิกธรรม",
|
| 117 |
+
"สัมผัส", "สัมมัตตะ", "สารณียธรรม", "สาสน์", "สิกขา",
|
| 118 |
+
"สุข", "สุขของคฤหัสถ์", "สุจริต", "สุทธาวาส", "สุทธิ",
|
| 119 |
+
"สุหทมิตร", "อกุศลกรรมบถ", "อกุศลมูล", "อกุศลวิตก",
|
| 120 |
+
"อคติ", "อธิปไตย", "อธิษฐาน", "อนันตริยกรรม",
|
| 121 |
+
"อนาคามี", "อนุตตริยะ", "อนุบุพพวิหาร", "อนุปุพพิกถา",
|
| 122 |
+
"อนุสติ", "อนุสัย", "อบาย", "อบายมุข",
|
| 123 |
+
"อปริหานิยธรรม", "อปัณณกปฏิปทา", "อปัสเสนะ",
|
| 124 |
+
"อภิญญา", "อภิฐาน", "อภิณหปัจจเวกขณ์", "อภิสังขาร",
|
| 125 |
+
"อรหันต์", "อริยทรัพย์", "อริยบุคคล", "อริยวงศ์",
|
| 126 |
+
"อริยวัฑฒิ", "อริยสัจจ์", "อรูป", "อสังขตลักษณะ",
|
| 127 |
+
"อสุภะ", "อัคคิ", "อัตถะ", "อันตคาหิกทิฏฐิ",
|
| 128 |
+
"อันตรายของภิกษุสามเณรผู้บวชใหม่", "อัปปมาทะ",
|
| 129 |
+
"อาการที่พระพุทธเจ้าทรงสั่งสอน", "อาจารย์",
|
| 130 |
+
"อานาปานสติ 16 ฐาน", "อายตนะภายนอก", "อายตนะภายใน",
|
| 131 |
+
"อายุสสธรรม", "อาวาสิกธรรม", "อาสวะ", "อาหาร",
|
| 132 |
+
"อิทธิบาท", "อุบาสกธรรม", "อุปกิเลส", "อุปัญญาตธรรม",
|
| 133 |
+
"อุปาทาน", "อุปาทารูป", "เจดีย์", "เจตนา", "เถรธรรม",
|
| 134 |
+
"เทพ", "เทวทูต", "เทศนา", "เบญจธรรม", "เมถุนสังโยค",
|
| 135 |
+
"เวทนา", "เวปุลลธรรม", "เวปุลละ", "เวสารัชชกรณธรรม",
|
| 136 |
+
"เวสารัชชะ", "โกศล", "โภควิภาค", "โภคอาทิยะ",
|
| 137 |
+
"โยคะ", "โยนิ", "โยนิโสมนสิการ", "โลก", "โลกธรรม",
|
| 138 |
+
"โลกุตตรธรรม", "โสดาบัน", "โสตาปัตติยังคะ", "โอฆะ",
|
| 139 |
+
"ไตรปิฎก",
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
|
| 143 |
class SearchService:
|
| 144 |
def __init__(self, db: SQLiteDB):
|
| 145 |
self.db = db
|
| 146 |
|
| 147 |
+
# ── Autocomplete suggestions ────────────────────────────────────
|
| 148 |
+
|
| 149 |
+
def get_suggestions(self, prefix: str, limit: int = 8) -> List[str]:
|
| 150 |
+
"""
|
| 151 |
+
Return autocomplete suggestions for `prefix`.
|
| 152 |
+
Priority: search_log (by frequency) → POPULAR_TERMS (curated).
|
| 153 |
+
"""
|
| 154 |
+
if not prefix or len(prefix.strip()) < 1:
|
| 155 |
+
return []
|
| 156 |
+
|
| 157 |
+
prefix = prefix.strip()
|
| 158 |
+
results: List[str] = []
|
| 159 |
+
seen: set[str] = set()
|
| 160 |
+
|
| 161 |
+
# 1. From search history (disk) — most searched first
|
| 162 |
+
try:
|
| 163 |
+
with self.db.get_disk_connection() as conn:
|
| 164 |
+
rows = conn.execute("""
|
| 165 |
+
SELECT query FROM search_log
|
| 166 |
+
WHERE query LIKE ? COLLATE NOCASE
|
| 167 |
+
ORDER BY count DESC, last_at DESC
|
| 168 |
+
LIMIT ?
|
| 169 |
+
""", (f"{prefix}%", limit)).fetchall()
|
| 170 |
+
for row in rows:
|
| 171 |
+
q = row["query"]
|
| 172 |
+
if q not in seen:
|
| 173 |
+
results.append(q)
|
| 174 |
+
seen.add(q)
|
| 175 |
+
except Exception:
|
| 176 |
+
pass
|
| 177 |
+
|
| 178 |
+
# 2. Fill remaining from POPULAR_TERMS
|
| 179 |
+
if len(results) < limit:
|
| 180 |
+
for term in POPULAR_TERMS:
|
| 181 |
+
if term.startswith(prefix) and term not in seen:
|
| 182 |
+
results.append(term)
|
| 183 |
+
seen.add(term)
|
| 184 |
+
if len(results) >= limit:
|
| 185 |
+
break
|
| 186 |
+
|
| 187 |
+
return results[:limit]
|
| 188 |
+
|
| 189 |
+
def log_search(self, query: str) -> None:
|
| 190 |
+
"""Upsert query into search_log (increment count if exists)."""
|
| 191 |
+
q = query.strip()
|
| 192 |
+
if not q:
|
| 193 |
+
return
|
| 194 |
+
try:
|
| 195 |
+
with self.db.get_disk_connection() as conn:
|
| 196 |
+
conn.execute("""
|
| 197 |
+
INSERT INTO search_log(query, count, last_at)
|
| 198 |
+
VALUES(?, 1, datetime('now','localtime'))
|
| 199 |
+
ON CONFLICT(query) DO UPDATE SET
|
| 200 |
+
count = count + 1,
|
| 201 |
+
last_at = datetime('now','localtime')
|
| 202 |
+
""", (q,))
|
| 203 |
+
except Exception:
|
| 204 |
+
pass # Never fail a search because of logging
|
| 205 |
+
|
| 206 |
def clean_text(self, text: str) -> str:
|
| 207 |
"""Port of v2.1 _clean(): strips line numbers, footnotes, separators."""
|
| 208 |
if not text:
|
|
|
|
| 354 |
|
| 355 |
time_ms = (time.time() - start_time) * 1000
|
| 356 |
|
| 357 |
+
response = SearchResponse(
|
| 358 |
query=corrected_q,
|
| 359 |
total_results=total_count,
|
| 360 |
results=paginated,
|
| 361 |
time_taken_ms=time_ms,
|
| 362 |
breakdown_by_volume=breakdown_data,
|
| 363 |
)
|
| 364 |
+
|
| 365 |
+
# Log after `with` block closes (non-blocking, won't affect response)
|
| 366 |
+
self.log_search(corrected_q)
|
| 367 |
+
return response
|
webapp/tipitaka-web/src/components/layout/NavDrawer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
import { useUIStore, useReaderStore, useThemeStore } from '../../stores/appStore';
|
| 3 |
import { useSearchStore } from '../../stores/searchStore';
|
| 4 |
import api from '../../lib/api';
|
|
@@ -22,11 +22,15 @@ const NavDrawer: React.FC = () => {
|
|
| 22 |
const {
|
| 23 |
query, setQuery, results, totalResults,
|
| 24 |
isLoading, performSearch, breakdown, timeTaken,
|
|
|
|
| 25 |
} = useSearchStore();
|
| 26 |
|
| 27 |
const [toc, setToc] = useState<any[]>([]);
|
| 28 |
const [loadingToc, setLoadingToc] = useState(false);
|
| 29 |
const [volumes, setVolumes] = useState<any[]>([]);
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
const s = DRAWER_STYLES[theme] ?? DRAWER_STYLES.dark;
|
| 32 |
|
|
@@ -49,8 +53,68 @@ const NavDrawer: React.FC = () => {
|
|
| 49 |
if (window.innerWidth < 1024) setNavOpen(false);
|
| 50 |
};
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
const handleSearch = (e: React.FormEvent) => {
|
| 53 |
e.preventDefault();
|
|
|
|
| 54 |
setActiveTab('search');
|
| 55 |
performSearch();
|
| 56 |
};
|
|
@@ -161,16 +225,49 @@ const NavDrawer: React.FC = () => {
|
|
| 161 |
{/* ── Search ── */}
|
| 162 |
{activeTab === 'search' && (
|
| 163 |
<div className="p-3 space-y-3">
|
| 164 |
-
<form onSubmit={handleSearch} className="relative">
|
| 165 |
<input
|
| 166 |
type="text"
|
| 167 |
value={query}
|
| 168 |
-
onChange={e =>
|
|
|
|
|
|
|
| 169 |
placeholder="ค้นหาข้อความ เช่น อริยสัจ…"
|
|
|
|
| 170 |
className={`w-full pl-9 pr-3 py-2.5 rounded-xl text-sm outline-none border ${s.border} ${s.input} ${s.text} focus:border-[#c8860a] transition-colors`}
|
| 171 |
/>
|
| 172 |
<Search size={16} className="absolute left-3 top-3 text-[#888]" />
|
| 173 |
{isLoading && <Loader2 size={14} className="absolute right-3 top-3.5 animate-spin text-[#c8860a]" />}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
</form>
|
| 175 |
|
| 176 |
{results.length > 0 && (
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
| 2 |
import { useUIStore, useReaderStore, useThemeStore } from '../../stores/appStore';
|
| 3 |
import { useSearchStore } from '../../stores/searchStore';
|
| 4 |
import api from '../../lib/api';
|
|
|
|
| 22 |
const {
|
| 23 |
query, setQuery, results, totalResults,
|
| 24 |
isLoading, performSearch, breakdown, timeTaken,
|
| 25 |
+
suggestions, showSuggestions, fetchSuggestions, setShowSuggestions,
|
| 26 |
} = useSearchStore();
|
| 27 |
|
| 28 |
const [toc, setToc] = useState<any[]>([]);
|
| 29 |
const [loadingToc, setLoadingToc] = useState(false);
|
| 30 |
const [volumes, setVolumes] = useState<any[]>([]);
|
| 31 |
+
const [activeSuggestion, setActiveSuggestion] = useState(-1);
|
| 32 |
+
const searchRef = useRef<HTMLDivElement>(null);
|
| 33 |
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 34 |
|
| 35 |
const s = DRAWER_STYLES[theme] ?? DRAWER_STYLES.dark;
|
| 36 |
|
|
|
|
| 53 |
if (window.innerWidth < 1024) setNavOpen(false);
|
| 54 |
};
|
| 55 |
|
| 56 |
+
// Debounced suggestion fetch
|
| 57 |
+
const handleQueryChange = useCallback((value: string) => {
|
| 58 |
+
setQuery(value);
|
| 59 |
+
setActiveSuggestion(-1);
|
| 60 |
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
| 61 |
+
debounceRef.current = setTimeout(() => {
|
| 62 |
+
fetchSuggestions(value);
|
| 63 |
+
}, 280);
|
| 64 |
+
}, [setQuery, fetchSuggestions]);
|
| 65 |
+
|
| 66 |
+
// Close suggestions on outside click
|
| 67 |
+
useEffect(() => {
|
| 68 |
+
const handler = (e: MouseEvent) => {
|
| 69 |
+
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
| 70 |
+
setShowSuggestions(false);
|
| 71 |
+
}
|
| 72 |
+
};
|
| 73 |
+
document.addEventListener('mousedown', handler);
|
| 74 |
+
return () => document.removeEventListener('mousedown', handler);
|
| 75 |
+
}, [setShowSuggestions]);
|
| 76 |
+
|
| 77 |
+
const selectSuggestion = (term: string) => {
|
| 78 |
+
setQuery(term);
|
| 79 |
+
setShowSuggestions(false);
|
| 80 |
+
setActiveSuggestion(-1);
|
| 81 |
+
setActiveTab('search');
|
| 82 |
+
setTimeout(() => performSearch(), 0);
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
| 86 |
+
if (!showSuggestions || suggestions.length === 0) {
|
| 87 |
+
if (e.key === 'Enter') {
|
| 88 |
+
e.preventDefault();
|
| 89 |
+
setActiveTab('search');
|
| 90 |
+
performSearch();
|
| 91 |
+
}
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
+
if (e.key === 'ArrowDown') {
|
| 95 |
+
e.preventDefault();
|
| 96 |
+
setActiveSuggestion(i => Math.min(i + 1, suggestions.length - 1));
|
| 97 |
+
} else if (e.key === 'ArrowUp') {
|
| 98 |
+
e.preventDefault();
|
| 99 |
+
setActiveSuggestion(i => Math.max(i - 1, -1));
|
| 100 |
+
} else if (e.key === 'Enter') {
|
| 101 |
+
e.preventDefault();
|
| 102 |
+
if (activeSuggestion >= 0) {
|
| 103 |
+
selectSuggestion(suggestions[activeSuggestion]);
|
| 104 |
+
} else {
|
| 105 |
+
setShowSuggestions(false);
|
| 106 |
+
setActiveTab('search');
|
| 107 |
+
performSearch();
|
| 108 |
+
}
|
| 109 |
+
} else if (e.key === 'Escape') {
|
| 110 |
+
setShowSuggestions(false);
|
| 111 |
+
setActiveSuggestion(-1);
|
| 112 |
+
}
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
const handleSearch = (e: React.FormEvent) => {
|
| 116 |
e.preventDefault();
|
| 117 |
+
setShowSuggestions(false);
|
| 118 |
setActiveTab('search');
|
| 119 |
performSearch();
|
| 120 |
};
|
|
|
|
| 225 |
{/* ── Search ── */}
|
| 226 |
{activeTab === 'search' && (
|
| 227 |
<div className="p-3 space-y-3">
|
| 228 |
+
<form onSubmit={handleSearch} className="relative" ref={searchRef}>
|
| 229 |
<input
|
| 230 |
type="text"
|
| 231 |
value={query}
|
| 232 |
+
onChange={e => handleQueryChange(e.target.value)}
|
| 233 |
+
onKeyDown={handleKeyDown}
|
| 234 |
+
onFocus={() => query && fetchSuggestions(query)}
|
| 235 |
placeholder="ค้นหาข้อความ เช่น อริยสัจ…"
|
| 236 |
+
autoComplete="off"
|
| 237 |
className={`w-full pl-9 pr-3 py-2.5 rounded-xl text-sm outline-none border ${s.border} ${s.input} ${s.text} focus:border-[#c8860a] transition-colors`}
|
| 238 |
/>
|
| 239 |
<Search size={16} className="absolute left-3 top-3 text-[#888]" />
|
| 240 |
{isLoading && <Loader2 size={14} className="absolute right-3 top-3.5 animate-spin text-[#c8860a]" />}
|
| 241 |
+
|
| 242 |
+
{/* Suggestions dropdown */}
|
| 243 |
+
<AnimatePresence>
|
| 244 |
+
{showSuggestions && suggestions.length > 0 && (
|
| 245 |
+
<motion.ul
|
| 246 |
+
initial={{ opacity: 0, y: -4 }}
|
| 247 |
+
animate={{ opacity: 1, y: 0 }}
|
| 248 |
+
exit={{ opacity: 0, y: -4 }}
|
| 249 |
+
transition={{ duration: 0.12 }}
|
| 250 |
+
className={`absolute left-0 right-0 top-full mt-1 z-50 rounded-xl border shadow-lg overflow-hidden ${s.bg} ${s.border}`}
|
| 251 |
+
>
|
| 252 |
+
{suggestions.map((term, i) => (
|
| 253 |
+
<li key={term}>
|
| 254 |
+
<button
|
| 255 |
+
type="button"
|
| 256 |
+
onMouseDown={() => selectSuggestion(term)}
|
| 257 |
+
className={`w-full text-left px-4 py-2.5 text-sm flex items-center gap-2 transition-colors ${
|
| 258 |
+
i === activeSuggestion
|
| 259 |
+
? 'bg-[#c8860a]/15 text-[#c8860a]'
|
| 260 |
+
: `${s.text} hover:bg-white/10`
|
| 261 |
+
}`}
|
| 262 |
+
>
|
| 263 |
+
<Search size={12} className="opacity-40 flex-shrink-0" />
|
| 264 |
+
{term}
|
| 265 |
+
</button>
|
| 266 |
+
</li>
|
| 267 |
+
))}
|
| 268 |
+
</motion.ul>
|
| 269 |
+
)}
|
| 270 |
+
</AnimatePresence>
|
| 271 |
</form>
|
| 272 |
|
| 273 |
{results.length > 0 && (
|
webapp/tipitaka-web/src/stores/searchStore.ts
CHANGED
|
@@ -20,11 +20,15 @@ interface SearchState {
|
|
| 20 |
breakdown: Record<number, VolumeStats>;
|
| 21 |
isLoading: boolean;
|
| 22 |
timeTaken: number;
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
// Actions
|
| 25 |
setQuery: (q: string) => void;
|
| 26 |
performSearch: () => Promise<void>;
|
| 27 |
clearResults: () => void;
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
export const useSearchStore = create<SearchState>((set, get) => ({
|
|
@@ -34,14 +38,31 @@ export const useSearchStore = create<SearchState>((set, get) => ({
|
|
| 34 |
breakdown: {},
|
| 35 |
isLoading: false,
|
| 36 |
timeTaken: 0,
|
|
|
|
|
|
|
| 37 |
|
| 38 |
setQuery: (q) => set({ query: q }),
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
performSearch: async () => {
|
| 41 |
const { query } = get();
|
| 42 |
if (!query.trim()) return;
|
| 43 |
|
| 44 |
-
set({ isLoading: true });
|
| 45 |
try {
|
| 46 |
const response = await api.get('/api/search', { params: { q: query } });
|
| 47 |
set({
|
|
@@ -57,5 +78,5 @@ export const useSearchStore = create<SearchState>((set, get) => ({
|
|
| 57 |
}
|
| 58 |
},
|
| 59 |
|
| 60 |
-
clearResults: () => set({ results: [], totalResults: 0, breakdown: {}, query: '' })
|
| 61 |
}));
|
|
|
|
| 20 |
breakdown: Record<number, VolumeStats>;
|
| 21 |
isLoading: boolean;
|
| 22 |
timeTaken: number;
|
| 23 |
+
suggestions: string[];
|
| 24 |
+
showSuggestions: boolean;
|
| 25 |
+
|
| 26 |
// Actions
|
| 27 |
setQuery: (q: string) => void;
|
| 28 |
performSearch: () => Promise<void>;
|
| 29 |
clearResults: () => void;
|
| 30 |
+
fetchSuggestions: (prefix: string) => Promise<void>;
|
| 31 |
+
setShowSuggestions: (v: boolean) => void;
|
| 32 |
}
|
| 33 |
|
| 34 |
export const useSearchStore = create<SearchState>((set, get) => ({
|
|
|
|
| 38 |
breakdown: {},
|
| 39 |
isLoading: false,
|
| 40 |
timeTaken: 0,
|
| 41 |
+
suggestions: [],
|
| 42 |
+
showSuggestions: false,
|
| 43 |
|
| 44 |
setQuery: (q) => set({ query: q }),
|
| 45 |
|
| 46 |
+
fetchSuggestions: async (prefix: string) => {
|
| 47 |
+
if (!prefix.trim()) {
|
| 48 |
+
set({ suggestions: [], showSuggestions: false });
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
try {
|
| 52 |
+
const res = await api.get('/api/search/suggestions', { params: { q: prefix, limit: 8 } });
|
| 53 |
+
set({ suggestions: res.data, showSuggestions: res.data.length > 0 });
|
| 54 |
+
} catch {
|
| 55 |
+
set({ suggestions: [], showSuggestions: false });
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
|
| 59 |
+
setShowSuggestions: (v) => set({ showSuggestions: v }),
|
| 60 |
+
|
| 61 |
performSearch: async () => {
|
| 62 |
const { query } = get();
|
| 63 |
if (!query.trim()) return;
|
| 64 |
|
| 65 |
+
set({ isLoading: true, showSuggestions: false });
|
| 66 |
try {
|
| 67 |
const response = await api.get('/api/search', { params: { q: query } });
|
| 68 |
set({
|
|
|
|
| 78 |
}
|
| 79 |
},
|
| 80 |
|
| 81 |
+
clearResults: () => set({ results: [], totalResults: 0, breakdown: {}, query: '', suggestions: [], showSuggestions: false })
|
| 82 |
}));
|