# Tipitaka Web Application Architecture V2 ## Vite + FastAPI + Qdrant — "Book Sanctuary + Floating AI Consultant" > **Version:** 2.4.0 > **Date:** 2026-05-20 > **Status:** Production (local dev, ready for HF Spaces) > **Based on:** Code at commit `f11cd44` --- ## 1. Executive Summary ### 1.1 Vision Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ ### 1.2 Key Changes #### V2.4.0 (2026-05-20) | Layer | V2.3.0 | V2.4.0 | |-------|--------|--------| | **AI Assistant UI** | Simple round button | **Speed Dial Floating Menu** — 5 quick actions (`AIFloatingMenu.tsx`) with responsive font size | | **Response Display** | Text in chat window | **Translucent Answer Card** — frosted-glass overlay (`viewMode: 'answer'`) with full height/width resize support | | **Disclaimer Warning** | Bottom/separate block | **Centered Title Bar Disclaimer** — Kalama Sutta warning absolute-centered in header to save vertical space | | **Focus Mode (Zoom)** | Latest assistant message only | **Individual Message Zoom** — maximize button on each assistant message bubble, responsive opacity | | **Mobile UX** | Cutoff on status bar | **Safe Area Insets** — top padding support for Focus Mode Header and menu buttons | | **Reader Navigation** | Keep scroll state | **Scroll Reset** — resets reader view to top when a new page loads, direction-aware transitions | | **PWA & Offline** | None | **Full Progressive Web App** — manifest, high-res Dharma wheel icons, Safari-compatible apple-touch-icons, Cache-Control headers | | **Commit** | `b8531b2` | `f11cd44` | #### V2.3.0 (2026-05-17) | Layer | V2.2.0 | V2.3.0 | |-------|--------|--------| | **Content Pipeline** | Separate `` per match | **Adjacent `` merge** — `textfn-text` merged into one heading | | **Marker Normalization** | Basic exclusion `[^\s\n\r>(\[]` | Added `]`, `.`, Thai digits `\u0E50-\u0E59` to prevent splits like `[๓๒]` + `\n๒.`, `๑๖.` + `\n.`, `๕๘.` + `\n(๑)` | | **Dialogue Markers** | `ถาม.`, `ตอบ.` only | Added `อนุ.`, `วิ.`, `ปฏิ.` for Abhidhamma/Paṭṭhāna pages | | **Abbrev (ย่อ) System** | LLM fallback on every click | **Pre-indexed DB** — 1,742 pages, 1,180,127 chars. Zero API calls. | | **Abbrev Lookup** | Via LLMService → lazy-loaded RAG | **Direct SQLite** — no import of RAG/LLM/Jina/ONNX | | **Ref Router** | Created fresh DB connection per lookup | Reuses existing `service.db` connection | | **Commit** | — | `b8531b2`| #### V2.2.0 (2026-05-15) | Layer | V2.1.0 | V2.2.0 | |-------|--------|--------| | **Embedding Model** | jina-embeddings-v3 (GGUF, Q4_K_M) | **jina-embeddings-v5-small-retrieval** (GGUF Q6_K / PyTorch) | | **Embedding Mode** | Ollama only | **Dual-mode:** Ollama (local) → sentence-transformers fallback (HF Space) | | **AI Popup Resize** | Right-edge only | **Vertical + Diagonal + Right-edge** resize, **persisted** panel size (localStorage) | | **Python Version** | 3.11 | 3.13 (ใหม่หลัง reinstall) | ### 1.3 Tech Stack ``` ┌──────────────────────────────────────────────────────────────────┐ │ CLIENT (Browser) │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Vite 6 + React 18 + TypeScript 5 │ │ │ │ Tailwind CSS v4 + @tailwindcss/typography │ │ │ │ Framer Motion (animations) + Zustand (state) │ │ │ │ Lucide React (icons) + React Markdown + remark-gfm │ │ │ └────────────────────────────────────────────────────────────┘ │ └──────────────────────────┬───────────────────────────────────────┘ │ HTTP / SSE Streaming ┌──────────────────────────┴───────────────────────────────────────┐ │ SERVER (FastAPI Python 3.13) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │ │ │ FastAPI │ │ Local │ │ Qdrant │ │ Embedding │ │ │ │ Routes │──│ Services │──│ (Vector) │──│ Provider │ │ │ └──────────┘ └──────────┘ └──────────────┘ └────────────┘ │ │ │ │ ┌────────────┐ │ │ │ ├── SQLite FTS5 (tipitaka_mcu.db) │ │ │ │ ├── ONNX Reranker (CPU, jina-reranker-v2)│ │ │ │ └── DeepSeek API (streaming) │ │ │ └── Static files (Served Vite build in production) │ └──────────────────────────────────────────────────────────────────┘ ``` **Embedding Providers:** - **Ollama** (local dev): `jina-embeddings-v5-small-retrieval` GGUF Q6_K (596MB) - **sentence-transformers** (HF Space / fallback): `jinaai/jina-embeddings-v5-text-small-retrieval` PyTorch - ทั้งคู่ produce 1024-dims vectors ค่า `EMBED_DIMS = 1024` --- ## 2. System Architecture ### 2.1 High-Level Component Diagram ```mermaid graph TB subgraph "Client Layer (Vite + React)" UI[User Interface] subgraph "Core Components" APP[AppShell
flex h-screen layout] ND[NavDrawer
320px sidebar] RP[ReaderPanel
Main reading area] RT[RightToolbar
44px controls strip] AIP[AIPopup
Draggable + resizable chat] SP[SelectionPopup
AI from selection] AIF[AIFloatingMenu
Speed Dial Menu] end subgraph "State (Zustand)" RS[ReaderStore
volume, page, content] TS[ThemeStore
theme, fontSize] US[UIStore
navOpen, activeTab] AS[AIStore
messages, mode, useRag, viewMode, width, height, pos] SS[SearchStore
query, results, breakdown] end end subgraph "API Layer (FastAPI)" subgraph "Routers" R1[GET /api/pages/volumes] R2[GET /api/pages/volumes/{id}/toc] R3[GET /api/pages/{vol}/{page}] R4[GET /api/search?q=...] R5[GET /api/search/suggestions] R6[POST /api/ask/stream (SSE)] R7[POST /api/ask (non-stream)] R8[GET /api/ask/rag-status] R9[GET /health] end subgraph "Services" S1[PageService
SQLite queries] S2[SearchService
FTS5 + LIKE fallback] S3[PaliUtils
autocorrect pipeline] S4[RAGService
Qdrant + Embedding + Reranker] S5[LLMService
DeepSeek streaming] S6[ONNXReranker
jina-reranker-v2] end subgraph "Data" DB[(SQLite
tipitaka_mcu.db)] VDB[(Qdrant
tipitaka_chunks
tipitaka_scripture)] SN[(Snapshots
.snapshot files)] end end UI --> APP APP --> ND & RP & RT & AIP & SP & AIF RP --> RS ND --> SS & US RT --> TS & RS AIP --> AS AIF --> AS R1 & R2 & R3 --> S1 --> DB R4 & R5 --> S2 --> DB R4 --> S3 R6 & R7 --> S5 S5 --> S4 --> VDB S5 --> DS[(DeepSeek API)] S4 --> EMB[(Embedding
Ollama v5 / ST fallback)] S4 --> RER[(ONNX Reranker)] ``` ### 2.2 Data Flow — Hybrid Search (RAG) ```mermaid sequenceDiagram actor User participant Browser participant API as FastAPI participant DB as SQLite FTS5 participant EMB as Embedding participant QD as Qdrant participant ONNX as ONNX Reranker participant LLM as DeepSeek API User->>Browser: ถาม AI (RAG ON) Browser->>API: POST /api/ask/stream {question, use_rag:true} par Phase 1: FTS5 API->>DB: MATCH query (30 results) DB-->>API: FTS rows + rank and Phase 2: Vector API->>EMB: query_embedding() alt Ollama available EMB->>OLL: jina-embeddings-v5 GGUF OLL-->>EMB: 1024-dims vector else Fallback EMB->>ST: sentence-transformers PyTorch ST-->>EMB: 1024-dims vector end EMB-->>API: embedding vector API->>QD: search with vector (30 results) QD-->>API: semantic hits end API->>API: Merge & deduplicate by (vol, page) API->>ONNX: Rerank candidates ONNX-->>API: Reordered scores API->>LLM: context + question (streaming) LLM-->>API: SSE chunks API-->>Browser: data: {chunk: "..."} ``` ### 2.3 Data Flow — Search ```mermaid sequenceDiagram actor User participant Browser participant API as FastAPI participant DB as SQLite User->>Browser: พิมพ์คำค้น Browser->>Browser: Debounce 280ms → fetchSuggestions Browser->>API: GET /api/search/suggestions?q=... API->>DB: search_log LIKE + POPULAR_TERMS DB-->>API: Suggestions API-->>Browser: ["อริยสัจ", "อริยสัจ 4", ...] User->>Browser: Enter Browser->>API: GET /api/search?q=อริยสัจ&limit=50 par FTS5 API->>DB: pages_fts MATCH "อริยสัจ" and LIKE Fallback API->>DB: content_text LIKE '%อริยสัจ%' end API->>API: Pali autocorrect pipeline API->>API: Merge & deduplicate API->>API: Highlight API-->>Browser: {total: 234, results: [...], breakdown: {...}} Browser->>Browser: Render cards + volume bars User->>Browser: คลิกผล → เล่ม 12 หน้า 45 Browser->>API: GET /api/pages/12/45 API-->>Browser: Page content ``` --- ## 3. Frontend Architecture ### 3.1 Project Structure ``` tipitaka-web/ ├── index.html # Vite entry with PWA viewport & iOS meta tags ├── vite.config.ts # Vite + proxy config ├── tsconfig.json / .app.json / .node.json ├── package.json ├── eslint.config.js │ ├── public/ # PWA Assets │ ├── manifest.json # Web app manifest for installability │ ├── favicon.ico / favicon.svg # App icons │ ├── icon-192.png / icon-512.png # High-res Dharma wheel icons │ └── apple-touch-icon*.png # Safari touch icons (precomposed & sized) │ └── src/ ├── main.tsx # React entry ├── App.tsx # only ├── index.css # Tailwind v4 @import "tailwindcss" │ ├── components/ │ ├── layout/ │ │ ├── AppShell.tsx # flex h-screen: NavDrawer + Reader + RightToolbar │ │ ├── NavDrawer.tsx # 320px sidebar: TOC / Search / Overview tabs │ │ ├── ReaderPanel.tsx # Main content area with swipe & vertical scroll nav │ │ └── MobileBottomBar.tsx # Mobile page nav │ │ │ ├── reader/ │ │ └── SelectionPopup.tsx # AI from text selection │ │ │ ├── toolbar/ │ │ └── RightToolbar.tsx # 44px fixed right: page nav, font size, theme │ │ │ └── ai/ │ ├── AIPopup.tsx # Draggable + resizable chat & translucent answer card │ └── AIFloatingMenu.tsx # Floating speed dial menu with 5 quick actions │ ├── hooks/ │ ├── useKeyboardNav.ts # ← → PgUp PgDown Space │ └── useSwipeNav.ts # Mobile swipe gestures │ ├── stores/ │ ├── appStore.ts # readerStore + themeStore + uiStore │ ├── aiStore.ts # AI messages, streaming, drag+resize state, viewMode, panelSize │ └── searchStore.ts # Search queries, results, suggestions │ └── lib/ └── api.ts # Axios instance (simple, no React Query) ``` ### 3.2 Key Components #### AppShell ```tsx // Layout: flex h-screen overflow-hidden // Structure: NavDrawer | Reader (flex-1) | RightToolbar (fixed 44px) // Overlays: AI Popup (fixed), Selection Popup (fixed), AIFloatingMenu (fixed), Mobile Bottom Bar // Theme: dynamic SHELL_BG per theme (dark/light/classic) ``` #### ReaderPanel ```tsx // Core reader — page-by-page navigation // Content rendered via dangerouslySetInnerHTML from content_html_formatted (not split by double newline) // → Backend pre-formats HTML via PageService.format_content_html() // First page (page 1) has special layout: split at "ขอนอบน้อม" homage // → TOP: centered, clean, with volume title in gold // → BOTTOM: text-justify with normal h4 styling // Body text alignment: justify with inter-character spacing (CSS: text-justify: inter-character) // Font size: 12-36px via themeStore // Line height: 2.1 // Theme-aware styles (3 themes: dark/light/classic) // End markers: extracted from raw tags containing "จบ" — rendered as --- marker text --- // AnimatePresence for page transitions (direction-aware) // Scroll reset: automatic scroll-to-top when navigating to a new page // Swipe support: ← next, → prev (mobile) ``` #### AIFloatingMenu [NEW] ```tsx // Floating Speed Dial menu collapsing into a single Sparkles/X icon // Expands upward to reveal 5 action buttons with tooltips (อธิบาย, สรุป, วิเคราะห์ธรรม, ประยุกต์ใช้, แชทสอบถาม) // Label font size: text-[13px] md:text-sm font-bold for high readability on all screens // Radial stagger animation using framer-motion // Actions 1-4 automatically send current page context and trigger 'answer' viewMode (Translucent Answer Card) // Action 5 switches to 'chat' viewMode (opens the main chat window) ``` #### AIPopup ```tsx // Floating, draggable assistant panel supporting two modes: // 1. 'chat' viewMode (full conversational interface) // 2. 'answer' viewMode (translucent glassmorphism card for quick AI summaries) // Three resize modes: right-edge (horizontal), bottom-edge (vertical), corner (diagonal) // Panel size (width, height) and coordinates (pos) are fully resizable and persisted in localStorage // Two AI models: fast (deepseek-chat) / reasoner (deepseek-reasoner) // RAG toggle pill (green/yellow/red dot readiness indicator) // Title Bar Disclaimer: Kalama Sutta warning is absolute-centered in Row 1 of header to save vertical space // Individual Message Fullscreen: Maximize button on each assistant message bubble, enabling focus mode on any previous message in the chat log. Oppacity is responsive (always visible on mobile, hover-only on desktop) // Safe Area Support: paddingTop calculation in Focus Mode Header preventing notched phones from cutting off buttons // Input + Send button // Width: 300px–80vw, default 400px ``` #### NavDrawer (320px) ```tsx // 3 tabs: TOC | Search | Overview // TOC: volume selector → list of sections (click to navigate) // Search: input → suggestions dropdown → results cards + volume bars // Overview: project info + tech stack badges // Slide animation: framer-motion (x: 0 or -320) // Mobile: overlay backdrop ``` #### RightToolbar (44px) ```tsx // Fixed right strip — controls from bottom: // Page number (tap to type jump) → +/- stepper // ↑ Prev / ↓ Next // ↑ Scroll to top // A+ / A− Font size // ☀/🌙/📜 Theme cycle (dark → light → classic) // Auto-hide at 20% opacity during scroll, restore on hover/idle 1.5s ``` #### AIPopup ```tsx // Floating, draggable chat panel // Three resize modes: right-edge drag (horizontal), bottom-edge drag (vertical), corner drag (diagonal) // Panel size persisted in localStorage // Two modes: fast (deepseek-chat) / reasoner (deepseek-reasoner) // RAG toggle pill (green/yellow/red dot indicator) // Focus mode: Maximize button → full-screen overlay (markdown rendering) // Prose-invert for dark mode readability // Disclaimer: 10s initial popup with Kalama Sutta reference // Persisted: drag position + panel width + panel height (localStorage) // Quick prompts: อธิบาย / สรุป / วิเคราะห์ธรรม / ประยุกต์ใช้ // Input + Send button // Width: 300px–80vw, default 400px ``` ### 3.3 State Management (Zustand) | Store | Key State | Persistence | |-------|-----------|-------------| | **ReaderStore** | currentVolume, currentPage, currentContent, totalPages | No | | **ThemeStore** | theme (dark/light/classic), fontSize (12-36) | No | | **UIStore** | isNavOpen, activeTab (toc/search/overview) | No | | **AIStore** | messages, isStreaming, isOpen, mode, useRag, ragReady, dragPos, panelWidth, panelHeight | dragPos + panelWidth + panelHeight → localStorage | | **SearchStore** | query, results, totalResults, isLoading, breakdown, timeTaken, suggestions | No | ### 3.4 Theming System Three themes with consistent tonal layering: | Element | Dark | Light | Classic | |---------|------|-------|---------| | **Shell bg** | `#1a1a2e` (Indigo Midnight) | `#f8f6f0` (Warm White) | `#f5edd8` (Cream Dark) | | **Reader bg** | `#1e1e36` | `#fdfaf5` | `#faf7f2` | | **Text** | `#e8e4da` | `#1a1a1a` | `#1a1a1a` | | **Toolbar bg** | `#222242` | `#e8e0c8` | `#e0d5b8` | | **Accent** | `#c8860a` (Dhamma Gold) — all themes | | | | **Muted text** | `#999` | `#666` | `#555` | --- ## 4. Backend Architecture ### 4.1 Project Structure ``` tipitaka-api/ ├── app/ │ ├── main.py # FastAPI app + lifespan (background init) │ ├── config.py # pydantic-settings (auto-path detection) │ ├── schemas.py # Pydantic models (Page, SearchResult, TOC, Volume) │ │ │ ├── routers/ │ │ ├── pages.py # /api/pages/* — volumes, TOC, page content │ │ ├── search.py # /api/search — FTS5 + LIKE + suggestions │ │ └── ai.py # /api/ask — streaming SSE + non-streaming + rag-status │ │ │ ├── services/ │ │ ├── page_service.py # Page retrieval + TOC dedup + blank detection + format_content_html() pipeline │ │ ├── search_service.py # FTS5 + LIKE + autocorrect + snippet + highlight │ │ ├── pali_utils.py # Pali autocorrect dict + Thai digit utils + PyThaiNLP │ │ ├── rag_service.py # Qdrant init + dual-mode embedding + hybrid query + rerank │ │ ├── llm_service.py # OpenAI-compatible streaming + RAG context injection │ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference │ │ │ └── database/ │ └── sqlite_db.py # SQLite connection + in-memory load + FTS5 + search_log │ ├── models/ │ └── jina-v2-onnx/ # ONNX reranker model files (~267MB) │ ├── tests/ │ ├── test_pali_utils.py # 90 assertions (autocorrect, Thai digits, etc.) │ ├── test_search_service.py # Search pipeline, merge, suggestions │ └── test_rag_service.py # Embedding cache test │ ├── download_assets.py # Downloads DB + snapshots + reranker from HF dataset ├── startup.sh # HF Spaces entrypoint ├── requirements.txt └── .env / .env.example ``` ### 4.2 API Endpoints | Method | Path | Description | |--------|------|-------------| | GET | `/api/pages/volumes` | List all volumes (id, volume_number, title, pitaka, total_pages) | | GET | `/api/pages/volumes/{id}/toc` | Table of contents (deduplicated, noise-filtered) | | GET | `/api/pages/{vol}/{page}` | Page content (text + metadata, blank detection) | | GET | `/api/search?q=&limit=&offset=` | Hybrid FTS5 + LIKE search with highlight + breakdown | | GET | `/api/search/suggestions?q=&limit=` | Autocomplete from search_log + POPULAR_TERMS | | POST | `/api/ask/stream` | AI streaming (SSE), with optional RAG context | | POST | `/api/ask` | Non-streaming AI (for Discord/Hermes integration) | | GET | `/api/ask/rag-status` | Qdrant readiness check (filesystem/server) | | GET | `/health` | Health check | ### 4.3 Search Pipeline (SearchService) ``` query → Pali Autocorrect Pipeline: Stage A: Explicit dict (PALI_CORRECTIONS_FULL) Stage B: PyThaiNLP autocorrect + similarity guard → Thai digit conversion → FTS5 MATCH (pages_fts, ranked) → LIKE fallback (unmatched results) → Merge & deduplicate by (volume_id, page_number) → Snippet generation (keyword-centered, sentence-bounded) → highlighting (keyword + Thai digit variant) → Volume breakdown calculation → Log to search_log (if results found) ``` ### 4.4 RAG Pipeline (RAGService) ``` query → Phase 1: FTS5 (30 results) → Phase 2: Embedding → Qdrant Vector (30 results, jina-embeddings-v5) ├── Try Ollama (jina-embeddings-v5 GGUF, timeout 5s) └── Fallback → sentence-transformers (jina-embeddings-v5 PyTorch) → Merge & deduplicate by (volume, page) → Phase 3: ONNX Reranker (jina-reranker-v2) → Sort by rerank_score → Top N results → formatted context ``` ### 4.5 AI Pipeline (LLMService) ``` question + context + history + mode → Inject page context + RAG hits (if use_rag=true) → System prompt (Buddhist expert, wiki-style, strict citation rules) → DeepSeek API streaming → SSE: data: {chunk: "..."} / data: {thinking: "..."} / data: {done: true} ``` **Model routing:** - `mode="fast"` → `LLM_MODEL_FAST` (deepseek-chat) - `mode="reasoner"` → `LLM_MODEL_REASONER` (deepseek-reasoner) ### 4.6 Page Processing Pipeline (PageService) `format_content_html()` — transforms raw HTML from SQLite into reader-ready HTML: ``` raw content_html → Strip ... → Strip @ footnote lines → Strip separator lines (===, ---, ___) → Strip separators (______, ===) → Convert headings

headings

(except tags containing "จบ" → hidden, used as end markers) → Remove footnote markers: [๑], [๑๒], [ก], [ข], [1] → Remove inline footnote refs: "๑-" (when preceded by non-space) → Remove leading item markers: "๑- " at line start → Clean section range: "๓.-๑๐." → "๓." → Convert , → Normalize whitespace ``` **First-page split** — ReaderPanel splits `content_html_formatted` at the `

` containing "ขอนอบน้อม" (homage): - **TOP:** everything up to/including homage → rendered centered, clean, no h4 background/border - **BOTTOM:** content after homage → rendered with `text-justify`, normal h4 styling - This works for **all 45 volumes** regardless of where L0/L1 sections appear relative to homage (fixes volume 34 where L0 appears before homage) **End markers** — extracted from raw `content_html` by finding `` tags containing "จบ": - Rendered in ReaderPanel as `--- marker text ---` below body content ### 4.7 Database #### SQLite (tipitaka_mcu.db) - Tables: `volumes`, `pages`, `contents` (TOC), `search_log` - FTS5: `pages_fts` (virtual table on `content_text`, `unicode61` tokenizer) - In-memory: Full DB loaded to `:memory:` via `sqlite3.backup()` at startup - Search log: Disk-persisted for autocomplete (frequency-ranked) - File size: ~238MB #### Qdrant - Two modes: **Local** (embedded, snapshot restore) or **Server** (remote connection) - Collections: - `tipitaka_chunks` (scripture chunks, ~41K points, 460MB snapshot) - `tipitaka_scripture` (raw scripture, ~25K points, 496MB snapshot) - Autodetect: Checks `localhost:6333` if not configured - Snapshot restore: Extracts `.snapshot` files to storage directory on first run - Embeding: **jina-embeddings-v5-small-retrieval** (1024 dims) — dual-mode - LRU cache: 256 entries (OrderedDict) — avoids re-embedding identical queries #### ONNX Reranker - Model: **jina-reranker-v2-base-multilingual** (ONNX export, CPU) - Location: `models/jina-v2-onnx/` (~267MB) - Fallback: Graceful if unavailable (skips reranking step) --- ## 5. Dual-Mode Embedding System (NEW in 2.2.0) ### 5.1 Motivation ปัญหา: บน local ใช้ Ollama ทำ embedding ได้ แต่ **HF Space ไม่มี Ollama** ทางแก้: เพิ่ม sentence-transformers fallback — เขียนใน `_get_embedding()` ของ RAGService ### 5.2 Strategy ```python def _get_embedding(self, text: str) -> list: # ✅ Try 1: Ollama (local dev) — timeout 5s try: response = httpx.post(f"{self.ollama_url}/api/embed", ...) return embedding except Exception: pass # → fallback # ✅ Try 2: sentence-transformers (HF Space / no Ollama) if self._st_model is None: from sentence_transformers import SentenceTransformer self._st_model = SentenceTransformer( "jinaai/jina-embeddings-v5-text-small-retrieval" ) return self._st_model.encode(text, normalize_embeddings=True).tolist() ``` ### 5.3 Model Details | | Ollama Mode | Sentence-Transformers Mode | |---|---|---| | **Model** | `jina-embeddings-v5-small-retrieval` (GGUF) | `jina-embeddings-v5-text-small-retrieval` (PyTorch) | | **Size** | 596MB (Q6_K) | ~600MB | | **Dimensions** | 1024 | 1024 | | **Load type** | External process (Ollama) | In-process (Python) | | **RAM usage** | ~700MB (Ollama process) | ~1.2GB | | **Speed** | ~50ms (network call) | ~100ms (first call slower) | | **Cache** | LRU 256 entries — both modes | Shared | ### 5.4 Configuration ```python # rag_service.py — constants OLLAMA_URL = "http://localhost:11434" EMBED_MODEL = "hf.co/jinaai/...-v5-small-retrieval-Q6_K.gguf" ST_EMBED_MODEL = "jinaai/jina-embeddings-v5-text-small-retrieval" EMBED_DIMS = 1024 # ตรวจสอบแล้วว่าทั้งสอง mode ใช้ 1024 dims ``` - `OLLAMA_URL` สามารถ override ได้ผ่าน environment variable - ถ้า `sentence-transformers` ก็ไม่ต้อง setup อะไรเพิ่ม — โหลด model auto จาก HF Hub --- ## 6. Deployment ### 6.1 Hugging Face Spaces ```dockerfile # Dockerfile — multi-stage build Stage 1: node:20-alpine → npm ci → vite build Stage 2: python:3.11-slim → pip install → startup.sh - Copies built dist/ - Runs startup.sh (download_assets.py → data files) - Exposes port 7860 ``` **startup.sh** downloads: - `tipitaka_mcu.db` (SQLite corpus, ~238MB) - `qdrant/tipitaka_chunks.snapshot` (vector index, ~460MB) - `qdrant/tipitaka_scripture.snapshot` (vector index, ~496MB) - ONNX reranker via huggingface_hub (~267MB) ทั้งหมดดึงมาจาก dataset: `dhammawatthumpra/tipitaka-storage` **Qdrant lock cleanup:** ก่อน start uvicorn, startup.sh ลบ Qdrant lock file ที่ค้างจาก session ก่อนหน้า: ```bash rm -f "$QDRANT_PATH/.lock" 2>/dev/null || true ``` ป้องกัน error `"already accessed by another instance"` หลัง HF Space restart ### 6.2 Environment Variables (.env) | Variable | Default | Description | |----------|---------|-------------| | `LLM_API_KEY` | `""` | DeepSeek API key | | `LLM_BASE_URL` | `https://api.deepseek.com` | OpenAI-compatible base URL | | `LLM_MODEL_FAST` | `deepseek-chat` | Fast mode model | | `LLM_MODEL_REASONER` | `deepseek-reasoner` | Reasoner mode model | | `DATA_DIR` | Auto-detect | Data directory for DB + Qdrant | | `DATABASE_PATH` | Auto-detect | SQLite DB path | | `QDRANT_PATH` | Auto-detect | Qdrant storage path | | `QDRANT_URL` | `None` | Remote Qdrant URL (optional) | | `SNAPSHOT_DIR` | Auto-detect | Snapshot restore directory | | `OLLAMA_URL` | `http://localhost:11434` | Ollama endpoint (override for HF/docker) | | `CORS_ORIGINS` | `"*"` | CORS allowed origins | | `SERVE_STATIC` | `false` | Serve built frontend (HF Spaces) | | `PORT` | `8000` | Server port | | `DEBUG` | `true` | Debug mode | ### 6.3 Path Auto-Detection Config intelligently resolves paths based on environment: ```python # Priority: # 1. Explicit env var # 2. Docker (if /app/data exists and not Windows) # 3. Local project data/ directory ``` --- ## 7. Key Design Decisions ### 7.1 Why Qdrant over ChromaDB? - **Persistence**: Qdrant stores on disk reliably; ChromaDB had data-loss issues - **Dual mode**: Local (embedded, no Docker) + Server (production) — same API - **Snapshot restore**: `.snapshot` files enable deployment without reindexing - **Performance**: Faster vector search with tunable `score_threshold` ### 7.2 Why jina-embeddings-v5 over jina-embeddings-v3? - **Newer model**: v5-small-retrieval (Oct 2024) vs v3 (Mar 2024) - **1024 dims เหมือนกัน**: ไม่ต้อง re-index Qdrant - **Thai/Pali accuracy**: Confirmed same dimension space, better retrieval quality - **ยังคงมี GGUF ผ่าน Ollama**: สำหรับ local dev - **v5 ยังมี PyTorch บน HF Hub**: `jinaai/jina-embeddings-v5-text-small-retrieval` ใช้กับ sentence-transformers ได้ ### 7.3 Why sentence-transformers Fallback? - **HF Space ไม่มี Ollama**: ต้องมี embedding source อื่น - **No extra dependencies**: `sentence-transformers>=3.0` + `torch>=2.0` มีใน requirements.txt อยู่แล้ว - **One model, two runtimes**: ใช้ jina-embeddings-v5 ทั้งใน Ollama (GGUF) และ PyTorch — vector space เดียวกัน - **Seamless fallback**: ถ้า Ollama ไม่ว่าง → ใช้ ST โดยอัตโนมัติ, user ไม่รู้สึก - **Benchmark**: Ollama OFF (15.9s) vs Ollama ON (22.3s) — fallback เร็วกว่าเพราะ in-process ### 7.4 Why Page-by-Page over Continuous Scroll? - **Simplicity**: Avoids virtual scrolling complexity - **Performance**: One page loaded at a time (no memory buildup) - **Navigation**: Clear page markers, prev/next predictable - **Swipe**: Mobile gesture ← → works naturally with page boundaries ### 7.5 Why Progressive Web App (PWA) Implementation? - **Immersive View (จิตวิเวก)**: Standalone display mode removes browser address bars and controls, creating an clean, distraction-free environment for scripture reading. - **Platform Integration**: Custom Dharma wheel icons and manifest settings provide native-like installability on mobile (iOS/Android) and desktop, including proper theme coloring matching the midnight dark mode. - **Safe Area Insets**: Safe area environment variables (`env(safe-area-inset-top)`) are utilized in headers to handle physical phone notches and overlays on mobile when installed. ### 7.6 Why Split First Page at "ขอนอบน้อม"? - **Universal**: Works for all 45 volumes — the homage line is the only consistent structural element on page 1 - **Volume 34 edge case**: L0 section header appears *before* the homage (unique among all volumes) — splitting at L0 would break. Splitting at homage handles this seamlessly - **Visual hierarchy**: Homage belongs to the top "ceremonial" zone (centered, clean); actual content starts below with normal styling ### 7.7 Why text-justify for Body Text? - **Reading experience**: Justified text mimics printed Tipitaka books, creating a familiar sacred-text feel - **CSS**: `text-justify: inter-character` ensures even spacing in Thai script (where inter-word gaps would look uneven due to lack of explicit word boundaries in Thai) - **User preference**: ผู้ใช้ request ให้จัด justify (2026-05-07) ### 7.8 Why Speed Dial Floating Menu & Translucent Answer Card? - **Cleaner Interface**: Replaced the permanent floating AI block/button with a single collapsible Sparkles icon to preserve the Book Sanctuary theme when the AI is not needed. - **Frosted Glass Card (Answer Card)**: Triggers only when executing quick functions (Explain, Summarize, Analyze, Apply). The frosted glass background blends naturally into the reader background without full-screen distraction. - **Complete Freedom of Resizing**: The panel size constraint was lifted (allowing full height/width expansion) to prevent text overflow issues and allow comfortable reading of long AI responses. ### 7.9 Why Centered Title Bar Disclaimer (Kalama Sutta)? - **Saves Vertical Screen Real Estate**: Instead of having a dedicated banner occupying vertical space in the reader or popup layout, the Kalama Sutta warning is absolute-centered in the top title bar of the AIPopup. - **Safety Reference**: Constantly reminds readers of the Kalama Sutta guidelines when interpreting AI output, without affecting content readability. ### 7.10 Why Zoom Button on Individual History Bubbles? - **Focus Mode Flexibility**: Instead of only allowing the latest AI response to be opened in full-screen Focus Mode, a zoom button was added to *every* assistant response bubble in the history. Users can now zoom into any past answer. - **Responsive Opacity**: Always visible (semi-transparent) on mobile devices for ease of touch interaction, and only visible on hover on desktops to keep the interface clean. ### 7.11 Why Resetting Scroll Position to Top on Page Load? - **Consistent Reading Position**: Previously, when navigating to the next/prev page, the scroll position did not reset, starting the reader mid-way down the page. Resetting it to `0` ensures a natural reading flow from the top header downwards. --- ## 8. Test Coverage | Module | Tests | What it covers | |--------|-------|----------------| | `test_pali_utils.py` | 90 assertions | Dict corrections, Thai digits, PyThaiNLP autocorrect, similarity guard | | `test_search_service.py` | ~15 | FTS5 + LIKE merge, autocomplete, suggestions priority | | `test_rag_service.py` | ~10 | Embedding LRU cache (hit/miss/eviction) | **Total: 35 tests — all passing**