Spaces:
Running
Running
Commit ·
351a621
1
Parent(s): abb8b56
docs: update V2 arch doc to 2.2.0 — jina-v5, ST fallback, diagonal resize
Browse files- .socraticodecontextartifacts.json +44 -0
- TIPITAKA_WEB_ARCHITECTURE_V2.md +147 -66
.socraticodecontextartifacts.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"artifacts": [
|
| 3 |
+
{
|
| 4 |
+
"name": "architecture-v2",
|
| 5 |
+
"path": "./TIPITAKA_WEB_ARCHITECTURE_V2.md",
|
| 6 |
+
"description": "Full V2 architecture — React + FastAPI + Qdrant + ONNX Reranker + DeepSeek"
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"name": "design-system",
|
| 10 |
+
"path": "./DESIGN.md",
|
| 11 |
+
"description": "Design system — colors, typography, components, themes, spacing"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"name": "product-vision",
|
| 15 |
+
"path": "./PRODUCT.md",
|
| 16 |
+
"description": "Product vision, target users (monks/scholars), brand personality, design principles"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"name": "api-schemas",
|
| 20 |
+
"path": "./webapp/tipitaka-api/app/schemas.py",
|
| 21 |
+
"description": "Pydantic API schemas — PageResponse, SearchResult, TOCItem, VolumeInfo"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"name": "app-config",
|
| 25 |
+
"path": "./webapp/tipitaka-api/app/config.py",
|
| 26 |
+
"description": "App settings — LLM provider, paths, CORS, Qdrant config"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"name": "rag-service",
|
| 30 |
+
"path": "./webapp/tipitaka-api/app/services/rag_service.py",
|
| 31 |
+
"description": "RAG Service — hybrid search (FTS5 + Qdrant vector + ONNX reranker), embedding with Ollama/sentence-transformers fallback"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"name": "docker-deployment",
|
| 35 |
+
"path": "./Dockerfile",
|
| 36 |
+
"description": "Multi-stage Docker build: React frontend + Python FastAPI backend, target HF Space port 7860"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"name": "download-assets",
|
| 40 |
+
"path": "./webapp/tipitaka-api/download_assets.py",
|
| 41 |
+
"description": "Downloads DB + Qdrant snapshots + ONNX reranker from HF dataset at startup"
|
| 42 |
+
}
|
| 43 |
+
]
|
| 44 |
+
}
|
TIPITAKA_WEB_ARCHITECTURE_V2.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
# Tipitaka Web Application Architecture V2
|
| 2 |
## Vite + FastAPI + Qdrant — "Book Sanctuary + Floating AI Consultant"
|
| 3 |
|
| 4 |
-
> **Version:** 2.
|
| 5 |
-
> **Date:** 2026-05-
|
| 6 |
> **Status:** Production (HF Spaces)
|
| 7 |
-
> **Based on:**
|
| 8 |
|
| 9 |
---
|
| 10 |
|
|
@@ -13,22 +13,14 @@
|
|
| 13 |
### 1.1 Vision
|
| 14 |
Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ
|
| 15 |
|
| 16 |
-
### 1.2 Key Changes from
|
| 17 |
-
|
| 18 |
-
| Layer |
|
| 19 |
-
|-------|--------
|
| 20 |
-
| **
|
| 21 |
-
| **
|
| 22 |
-
| **
|
| 23 |
-
| **
|
| 24 |
-
| **Reranker** | None | **jina-reranker-v2-base-multilingual** (ONNX, CPU) |
|
| 25 |
-
| **Frontend CSS** | Tailwind v3 + shadcn/ui | **Tailwind CSS v4 + `@tailwindcss/typography`** |
|
| 26 |
-
| **State Management** | Zustand + React Query | **Zustand only** (lightweight) |
|
| 27 |
-
| **PWA / Offline** | Planned | **Not implemented** (server-dependant) |
|
| 28 |
-
| **Continuous Scroll** | Planned | **Page-by-page** (prev/next/swipe) |
|
| 29 |
-
| **Deployment** | Local server | **HF Spaces** (Docker multi-stage) |
|
| 30 |
-
| **AI Provider** | DeepSeek / Local | **DeepSeek only** (provider-agnostic via OpenAI client) |
|
| 31 |
-
| **Embedding Cache** | None | **LRU cache** (256 entries, OrderedDict) |
|
| 32 |
|
| 33 |
### 1.3 Tech Stack
|
| 34 |
|
|
@@ -44,20 +36,25 @@ Web Application สำหรับอ่านพระไตรปิฎก ม
|
|
| 44 |
└──────────────────────────┬───────────────────────────────────────┘
|
| 45 |
│ HTTP / SSE Streaming
|
| 46 |
┌──────────────────────────┴───────────────────────────────────────┐
|
| 47 |
-
│ SERVER (FastAPI Python 3.
|
| 48 |
│ │
|
| 49 |
-
│ ┌──────────┐ ┌──────────┐ ┌──────────
|
| 50 |
-
│ │ FastAPI │ │ Local │ │ Qdrant
|
| 51 |
-
│ │ Routes │──│ Services │──│ (Vector)
|
| 52 |
-
│ └──────────┘ └──────────┘ └──────────
|
| 53 |
-
│ │ │
|
| 54 |
-
│ │ ├── SQLite FTS5 (tipitaka_mcu.db)
|
| 55 |
-
│ │ ├── ONNX Reranker (CPU, jina-reranker-v2) │
|
| 56 |
-
│ │ └── DeepSeek API (streaming)
|
| 57 |
│ └── Static files (Served Vite build in production) │
|
| 58 |
└──────────────────────────────────────────────────────────────────┘
|
| 59 |
```
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
---
|
| 62 |
|
| 63 |
## 2. System Architecture
|
|
@@ -73,14 +70,14 @@ graph TB
|
|
| 73 |
ND[NavDrawer<br/>320px sidebar]
|
| 74 |
RP[ReaderPanel<br/>Main reading area]
|
| 75 |
RT[RightToolbar<br/>44px controls strip]
|
| 76 |
-
AIP[AIPopup<br/>Draggable chat]
|
| 77 |
SP[SelectionPopup<br/>AI from selection]
|
| 78 |
end
|
| 79 |
subgraph "State (Zustand)"
|
| 80 |
RS[ReaderStore<br/>volume, page, content]
|
| 81 |
TS[ThemeStore<br/>theme, fontSize]
|
| 82 |
US[UIStore<br/>navOpen, activeTab]
|
| 83 |
-
AS[AIStore<br/>messages, mode, width, pos]
|
| 84 |
SS[SearchStore<br/>query, results, breakdown]
|
| 85 |
end
|
| 86 |
end
|
|
@@ -107,7 +104,7 @@ graph TB
|
|
| 107 |
end
|
| 108 |
subgraph "Data"
|
| 109 |
DB[(SQLite<br/>tipitaka_mcu.db)]
|
| 110 |
-
VDB[(Qdrant<br/>tipitaka_chunks)]
|
| 111 |
SN[(Snapshots<br/>.snapshot files)]
|
| 112 |
end
|
| 113 |
end
|
|
@@ -124,7 +121,7 @@ graph TB
|
|
| 124 |
R6 & R7 --> S5
|
| 125 |
S5 --> S4 --> VDB
|
| 126 |
S5 --> DS[(DeepSeek API)]
|
| 127 |
-
S4 -->
|
| 128 |
S4 --> RER[(ONNX Reranker)]
|
| 129 |
```
|
| 130 |
|
|
@@ -136,6 +133,7 @@ sequenceDiagram
|
|
| 136 |
participant Browser
|
| 137 |
participant API as FastAPI
|
| 138 |
participant DB as SQLite FTS5
|
|
|
|
| 139 |
participant QD as Qdrant
|
| 140 |
participant ONNX as ONNX Reranker
|
| 141 |
participant LLM as DeepSeek API
|
|
@@ -147,8 +145,17 @@ sequenceDiagram
|
|
| 147 |
API->>DB: MATCH query (30 results)
|
| 148 |
DB-->>API: FTS rows + rank
|
| 149 |
and Phase 2: Vector
|
| 150 |
-
API->>
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
end
|
| 153 |
|
| 154 |
API->>API: Merge & deduplicate by (vol, page)
|
|
@@ -229,7 +236,7 @@ tipitaka-web/
|
|
| 229 |
│ │ └── RightToolbar.tsx # 44px fixed right: page nav, font size, theme
|
| 230 |
│ │
|
| 231 |
│ └── ai/
|
| 232 |
-
│ └── AIPopup.tsx # Draggable + resizable chat
|
| 233 |
│
|
| 234 |
├── hooks/
|
| 235 |
│ ├── useKeyboardNav.ts # ← → PgUp PgDown Space
|
|
@@ -237,7 +244,7 @@ tipitaka-web/
|
|
| 237 |
│
|
| 238 |
├── stores/
|
| 239 |
│ ├── appStore.ts # readerStore + themeStore + uiStore
|
| 240 |
-
│ ├── aiStore.ts # AI messages, streaming, drag+resize state
|
| 241 |
│ └── searchStore.ts # Search queries, results, suggestions
|
| 242 |
│
|
| 243 |
└── lib/
|
|
@@ -294,13 +301,15 @@ tipitaka-web/
|
|
| 294 |
|
| 295 |
#### AIPopup
|
| 296 |
```tsx
|
| 297 |
-
// Floating, draggable
|
| 298 |
-
//
|
|
|
|
|
|
|
| 299 |
// RAG toggle pill (green/yellow/red dot indicator)
|
| 300 |
// Focus mode: Maximize button → full-screen overlay (markdown rendering)
|
| 301 |
// Prose-invert for dark mode readability
|
| 302 |
// Disclaimer: 10s initial popup with Kalama Sutta reference
|
| 303 |
-
// Persisted: drag position + panel width (localStorage)
|
| 304 |
// Quick prompts: อธิบาย / สรุป / วิเคราะห์ธรรม / ประยุกต์ใช้
|
| 305 |
// Input + Send button
|
| 306 |
// Width: 300px–80vw, default 400px
|
|
@@ -313,7 +322,7 @@ tipitaka-web/
|
|
| 313 |
| **ReaderStore** | currentVolume, currentPage, currentContent, totalPages | No |
|
| 314 |
| **ThemeStore** | theme (dark/light/classic), fontSize (12-36) | No |
|
| 315 |
| **UIStore** | isNavOpen, activeTab (toc/search/overview) | No |
|
| 316 |
-
| **AIStore** | messages, isStreaming, isOpen, mode, useRag, ragReady, dragPos, panelWidth | dragPos + panelWidth → localStorage |
|
| 317 |
| **SearchStore** | query, results, totalResults, isLoading, breakdown, timeTaken, suggestions | No |
|
| 318 |
|
| 319 |
### 3.4 Theming System
|
|
@@ -351,7 +360,7 @@ tipitaka-api/
|
|
| 351 |
│ │ ├── page_service.py # Page retrieval + TOC dedup + blank detection + format_content_html() pipeline
|
| 352 |
│ │ ├── search_service.py # FTS5 + LIKE + autocorrect + snippet + highlight
|
| 353 |
│ │ ├── pali_utils.py # Pali autocorrect dict + Thai digit utils + PyThaiNLP
|
| 354 |
-
│ │ ├── rag_service.py # Qdrant init + embedding + hybrid query + rerank
|
| 355 |
│ │ ├── llm_service.py # OpenAI-compatible streaming + RAG context injection
|
| 356 |
│ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference
|
| 357 |
│ │
|
|
@@ -359,14 +368,14 @@ tipitaka-api/
|
|
| 359 |
│ └── sqlite_db.py # SQLite connection + in-memory load + FTS5 + search_log
|
| 360 |
│
|
| 361 |
├── models/
|
| 362 |
-
│
|
| 363 |
-
│ └── jina-v2-onnx/ # ONNX model files
|
| 364 |
│
|
| 365 |
├── tests/
|
| 366 |
│ ├── test_pali_utils.py # 90 assertions (autocorrect, Thai digits, etc.)
|
| 367 |
│ ├── test_search_service.py # Search pipeline, merge, suggestions
|
| 368 |
│ └── test_rag_service.py # Embedding cache test
|
| 369 |
│
|
|
|
|
| 370 |
├── startup.sh # HF Spaces entrypoint
|
| 371 |
├── requirements.txt
|
| 372 |
└── .env / .env.example
|
|
@@ -407,7 +416,9 @@ query → Pali Autocorrect Pipeline:
|
|
| 407 |
|
| 408 |
```
|
| 409 |
query → Phase 1: FTS5 (30 results)
|
| 410 |
-
→ Phase 2: Qdrant Vector (30 results, jina-embeddings-
|
|
|
|
|
|
|
| 411 |
→ Merge & deduplicate by (volume, page)
|
| 412 |
→ Phase 3: ONNX Reranker (jina-reranker-v2)
|
| 413 |
→ Sort by rerank_score
|
|
@@ -463,25 +474,82 @@ raw content_html
|
|
| 463 |
- FTS5: `pages_fts` (virtual table on `content_text`, `unicode61` tokenizer)
|
| 464 |
- In-memory: Full DB loaded to `:memory:` via `sqlite3.backup()` at startup
|
| 465 |
- Search log: Disk-persisted for autocomplete (frequency-ranked)
|
|
|
|
| 466 |
|
| 467 |
#### Qdrant
|
| 468 |
- Two modes: **Local** (embedded, snapshot restore) or **Server** (remote connection)
|
| 469 |
-
- Collections:
|
|
|
|
|
|
|
| 470 |
- Autodetect: Checks `localhost:6333` if not configured
|
| 471 |
- Snapshot restore: Extracts `.snapshot` files to storage directory on first run
|
| 472 |
-
-
|
| 473 |
- LRU cache: 256 entries (OrderedDict) — avoids re-embedding identical queries
|
| 474 |
|
| 475 |
#### ONNX Reranker
|
| 476 |
- Model: **jina-reranker-v2-base-multilingual** (ONNX export, CPU)
|
| 477 |
-
- Location: `models/jina-v2-onnx/`
|
| 478 |
- Fallback: Graceful if unavailable (skips reranking step)
|
| 479 |
|
| 480 |
---
|
| 481 |
|
| 482 |
-
## 5.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
-
### 5.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
```dockerfile
|
| 487 |
# Dockerfile — multi-stage build
|
|
@@ -493,9 +561,12 @@ Stage 2: python:3.11-slim → pip install → startup.sh
|
|
| 493 |
```
|
| 494 |
|
| 495 |
**startup.sh** downloads:
|
| 496 |
-
- `tipitaka_mcu.db` (SQLite corpus, ~
|
| 497 |
-
- `
|
| 498 |
-
- `
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
**Qdrant lock cleanup:** ก่อน start uvicorn, startup.sh ลบ Qdrant lock file ที่ค้างจาก session ก่อนหน้า:
|
| 501 |
```bash
|
|
@@ -503,7 +574,7 @@ rm -f "$QDRANT_PATH/.lock" 2>/dev/null || true
|
|
| 503 |
```
|
| 504 |
ป้องกัน error `"already accessed by another instance"` หลัง HF Space restart
|
| 505 |
|
| 506 |
-
###
|
| 507 |
|
| 508 |
| Variable | Default | Description |
|
| 509 |
|----------|---------|-------------|
|
|
@@ -516,12 +587,13 @@ rm -f "$QDRANT_PATH/.lock" 2>/dev/null || true
|
|
| 516 |
| `QDRANT_PATH` | Auto-detect | Qdrant storage path |
|
| 517 |
| `QDRANT_URL` | `None` | Remote Qdrant URL (optional) |
|
| 518 |
| `SNAPSHOT_DIR` | Auto-detect | Snapshot restore directory |
|
|
|
|
| 519 |
| `CORS_ORIGINS` | `"*"` | CORS allowed origins |
|
| 520 |
| `SERVE_STATIC` | `false` | Serve built frontend (HF Spaces) |
|
| 521 |
| `PORT` | `8000` | Server port |
|
| 522 |
| `DEBUG` | `true` | Debug mode |
|
| 523 |
|
| 524 |
-
###
|
| 525 |
|
| 526 |
Config intelligently resolves paths based on environment:
|
| 527 |
|
|
@@ -534,43 +606,52 @@ Config intelligently resolves paths based on environment:
|
|
| 534 |
|
| 535 |
---
|
| 536 |
|
| 537 |
-
##
|
| 538 |
|
| 539 |
-
###
|
| 540 |
- **Persistence**: Qdrant stores on disk reliably; ChromaDB had data-loss issues
|
| 541 |
- **Dual mode**: Local (embedded, no Docker) + Server (production) — same API
|
| 542 |
- **Snapshot restore**: `.snapshot` files enable deployment without reindexing
|
| 543 |
- **Performance**: Faster vector search with tunable `score_threshold`
|
| 544 |
|
| 545 |
-
###
|
| 546 |
-
- **
|
| 547 |
-
- **
|
| 548 |
-
- **
|
| 549 |
-
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
- **Simplicity**: Avoids virtual scrolling complexity
|
| 552 |
- **Performance**: One page loaded at a time (no memory buildup)
|
| 553 |
- **Navigation**: Clear page markers, prev/next predictable
|
| 554 |
- **Swipe**: Mobile gesture ← → works naturally with page boundaries
|
| 555 |
|
| 556 |
-
###
|
| 557 |
- **Content is dynamic**: Pages rendered with AI context and search results
|
| 558 |
- **Token-based auth**: Not applicable (public content, but still server-dependant)
|
| 559 |
- **Snapshot data too large**: 250MB+ not practical for offline storage
|
| 560 |
|
| 561 |
-
###
|
| 562 |
- **Universal**: Works for all 45 volumes — the homage line is the only consistent structural element on page 1
|
| 563 |
- **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
|
| 564 |
- **Visual hierarchy**: Homage belongs to the top "ceremonial" zone (centered, clean); actual content starts below with normal styling
|
| 565 |
|
| 566 |
-
###
|
| 567 |
- **Reading experience**: Justified text mimics printed Tipitaka books, creating a familiar sacred-text feel
|
| 568 |
- **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)
|
| 569 |
- **User preference**: ผู้ใช้ request ให้จัด justify (2026-05-07)
|
| 570 |
|
| 571 |
---
|
| 572 |
|
| 573 |
-
##
|
| 574 |
|
| 575 |
| Module | Tests | What it covers |
|
| 576 |
|--------|-------|----------------|
|
|
|
|
| 1 |
# Tipitaka Web Application Architecture V2
|
| 2 |
## Vite + FastAPI + Qdrant — "Book Sanctuary + Floating AI Consultant"
|
| 3 |
|
| 4 |
+
> **Version:** 2.2.0
|
| 5 |
+
> **Date:** 2026-05-15
|
| 6 |
> **Status:** Production (HF Spaces)
|
| 7 |
+
> **Based on:** TIPITAKA_WEB_ARCHITECTURE_V2.md (2.1.0), actual code base at commit `abb8b56`
|
| 8 |
|
| 9 |
---
|
| 10 |
|
|
|
|
| 13 |
### 1.1 Vision
|
| 14 |
Web Application สำหรับอ่านพระไตรปิฎก มจร. ที่ให้ประสบการณ์การอ่านแบบจิตวิเวก โดยมี AI เป็นผู้ช่วยลอยตัวที่ไม่รบกวนสมาธิ
|
| 15 |
|
| 16 |
+
### 1.2 Key Changes from V2.1.0
|
| 17 |
+
|
| 18 |
+
| Layer | V2.1.0 | V2.2.0 |
|
| 19 |
+
|-------|--------|--------|
|
| 20 |
+
| **Embedding Model** | jina-embeddings-v3 (GGUF, Q4_K_M) | **jina-embeddings-v5-small-retrieval** (GGUF Q6_K / PyTorch) |
|
| 21 |
+
| **Embedding Mode** | Ollama only | **Dual-mode:** Ollama (local) → sentence-transformers fallback (HF Space) |
|
| 22 |
+
| **AI Popup Resize** | Right-edge only | **Vertical + Diagonal + Right-edge** resize, **persisted** panel size (localStorage) |
|
| 23 |
+
| **Python Version** | 3.11 | 3.13 (ใหม่หลัง reinstall) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
### 1.3 Tech Stack
|
| 26 |
|
|
|
|
| 36 |
└──────────────────────────┬───────────────────────────────────────┘
|
| 37 |
│ HTTP / SSE Streaming
|
| 38 |
┌──────────────────────────┴───────────────────────────────────────┐
|
| 39 |
+
│ SERVER (FastAPI Python 3.13) │
|
| 40 |
│ │
|
| 41 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
|
| 42 |
+
│ │ FastAPI │ │ Local │ │ Qdrant │ │ Embedding │ │
|
| 43 |
+
│ │ Routes │──│ Services │──│ (Vector) │──│ Provider │ │
|
| 44 |
+
│ └──────────┘ └──────────┘ └──────────────┘ └────────────┘ │
|
| 45 |
+
│ │ │ ┌────────────┐ │
|
| 46 |
+
│ │ ├── SQLite FTS5 (tipitaka_mcu.db) │ │
|
| 47 |
+
│ │ ├── ONNX Reranker (CPU, jina-reranker-v2)│ │
|
| 48 |
+
│ │ └── DeepSeek API (streaming) │ │
|
| 49 |
│ └── Static files (Served Vite build in production) │
|
| 50 |
└──────────────────────────────────────────────────────────────────┘
|
| 51 |
```
|
| 52 |
|
| 53 |
+
**Embedding Providers:**
|
| 54 |
+
- **Ollama** (local dev): `jina-embeddings-v5-small-retrieval` GGUF Q6_K (596MB)
|
| 55 |
+
- **sentence-transformers** (HF Space / fallback): `jinaai/jina-embeddings-v5-text-small-retrieval` PyTorch
|
| 56 |
+
- ทั้งคู่ produce 1024-dims vectors ค่า `EMBED_DIMS = 1024`
|
| 57 |
+
|
| 58 |
---
|
| 59 |
|
| 60 |
## 2. System Architecture
|
|
|
|
| 70 |
ND[NavDrawer<br/>320px sidebar]
|
| 71 |
RP[ReaderPanel<br/>Main reading area]
|
| 72 |
RT[RightToolbar<br/>44px controls strip]
|
| 73 |
+
AIP[AIPopup<br/>Draggable + resizable chat]
|
| 74 |
SP[SelectionPopup<br/>AI from selection]
|
| 75 |
end
|
| 76 |
subgraph "State (Zustand)"
|
| 77 |
RS[ReaderStore<br/>volume, page, content]
|
| 78 |
TS[ThemeStore<br/>theme, fontSize]
|
| 79 |
US[UIStore<br/>navOpen, activeTab]
|
| 80 |
+
AS[AIStore<br/>messages, mode, width, pos, panelSize]
|
| 81 |
SS[SearchStore<br/>query, results, breakdown]
|
| 82 |
end
|
| 83 |
end
|
|
|
|
| 104 |
end
|
| 105 |
subgraph "Data"
|
| 106 |
DB[(SQLite<br/>tipitaka_mcu.db)]
|
| 107 |
+
VDB[(Qdrant<br/>tipitaka_chunks<br/>tipitaka_scripture)]
|
| 108 |
SN[(Snapshots<br/>.snapshot files)]
|
| 109 |
end
|
| 110 |
end
|
|
|
|
| 121 |
R6 & R7 --> S5
|
| 122 |
S5 --> S4 --> VDB
|
| 123 |
S5 --> DS[(DeepSeek API)]
|
| 124 |
+
S4 --> EMB[(Embedding<br/>Ollama v5 / ST fallback)]
|
| 125 |
S4 --> RER[(ONNX Reranker)]
|
| 126 |
```
|
| 127 |
|
|
|
|
| 133 |
participant Browser
|
| 134 |
participant API as FastAPI
|
| 135 |
participant DB as SQLite FTS5
|
| 136 |
+
participant EMB as Embedding
|
| 137 |
participant QD as Qdrant
|
| 138 |
participant ONNX as ONNX Reranker
|
| 139 |
participant LLM as DeepSeek API
|
|
|
|
| 145 |
API->>DB: MATCH query (30 results)
|
| 146 |
DB-->>API: FTS rows + rank
|
| 147 |
and Phase 2: Vector
|
| 148 |
+
API->>EMB: query_embedding()
|
| 149 |
+
alt Ollama available
|
| 150 |
+
EMB->>OLL: jina-embeddings-v5 GGUF
|
| 151 |
+
OLL-->>EMB: 1024-dims vector
|
| 152 |
+
else Fallback
|
| 153 |
+
EMB->>ST: sentence-transformers PyTorch
|
| 154 |
+
ST-->>EMB: 1024-dims vector
|
| 155 |
+
end
|
| 156 |
+
EMB-->>API: embedding vector
|
| 157 |
+
API->>QD: search with vector (30 results)
|
| 158 |
+
QD-->>API: semantic hits
|
| 159 |
end
|
| 160 |
|
| 161 |
API->>API: Merge & deduplicate by (vol, page)
|
|
|
|
| 236 |
│ │ └── RightToolbar.tsx # 44px fixed right: page nav, font size, theme
|
| 237 |
│ │
|
| 238 |
│ └── ai/
|
| 239 |
+
│ └── AIPopup.tsx # Draggable + vertically/diagonally resizable chat
|
| 240 |
│
|
| 241 |
├── hooks/
|
| 242 |
│ ├── useKeyboardNav.ts # ← → PgUp PgDown Space
|
|
|
|
| 244 |
│
|
| 245 |
├── stores/
|
| 246 |
│ ├── appStore.ts # readerStore + themeStore + uiStore
|
| 247 |
+
│ ├── aiStore.ts # AI messages, streaming, drag+resize state, panelSize
|
| 248 |
│ └── searchStore.ts # Search queries, results, suggestions
|
| 249 |
│
|
| 250 |
└── lib/
|
|
|
|
| 301 |
|
| 302 |
#### AIPopup
|
| 303 |
```tsx
|
| 304 |
+
// Floating, draggable chat panel
|
| 305 |
+
// Three resize modes: right-edge drag (horizontal), bottom-edge drag (vertical), corner drag (diagonal)
|
| 306 |
+
// Panel size persisted in localStorage
|
| 307 |
+
// Two modes: fast (deepseek-chat) / reasoner (deepseek-reasoner)
|
| 308 |
// RAG toggle pill (green/yellow/red dot indicator)
|
| 309 |
// Focus mode: Maximize button → full-screen overlay (markdown rendering)
|
| 310 |
// Prose-invert for dark mode readability
|
| 311 |
// Disclaimer: 10s initial popup with Kalama Sutta reference
|
| 312 |
+
// Persisted: drag position + panel width + panel height (localStorage)
|
| 313 |
// Quick prompts: อธิบาย / สรุป / วิเคราะห์ธรรม / ประยุกต์ใช้
|
| 314 |
// Input + Send button
|
| 315 |
// Width: 300px–80vw, default 400px
|
|
|
|
| 322 |
| **ReaderStore** | currentVolume, currentPage, currentContent, totalPages | No |
|
| 323 |
| **ThemeStore** | theme (dark/light/classic), fontSize (12-36) | No |
|
| 324 |
| **UIStore** | isNavOpen, activeTab (toc/search/overview) | No |
|
| 325 |
+
| **AIStore** | messages, isStreaming, isOpen, mode, useRag, ragReady, dragPos, panelWidth, panelHeight | dragPos + panelWidth + panelHeight → localStorage |
|
| 326 |
| **SearchStore** | query, results, totalResults, isLoading, breakdown, timeTaken, suggestions | No |
|
| 327 |
|
| 328 |
### 3.4 Theming System
|
|
|
|
| 360 |
│ │ ├── page_service.py # Page retrieval + TOC dedup + blank detection + format_content_html() pipeline
|
| 361 |
│ │ ├── search_service.py # FTS5 + LIKE + autocorrect + snippet + highlight
|
| 362 |
│ │ ├── pali_utils.py # Pali autocorrect dict + Thai digit utils + PyThaiNLP
|
| 363 |
+
│ │ ├── rag_service.py # Qdrant init + dual-mode embedding + hybrid query + rerank
|
| 364 |
│ │ ├── llm_service.py # OpenAI-compatible streaming + RAG context injection
|
| 365 |
│ │ └── onnx_reranker.py # jina-reranker-v2 ONNX CPU inference
|
| 366 |
│ │
|
|
|
|
| 368 |
│ └── sqlite_db.py # SQLite connection + in-memory load + FTS5 + search_log
|
| 369 |
│
|
| 370 |
├── models/
|
| 371 |
+
│ └── jina-v2-onnx/ # ONNX reranker model files (~267MB)
|
|
|
|
| 372 |
│
|
| 373 |
├── tests/
|
| 374 |
│ ├── test_pali_utils.py # 90 assertions (autocorrect, Thai digits, etc.)
|
| 375 |
│ ├── test_search_service.py # Search pipeline, merge, suggestions
|
| 376 |
│ └── test_rag_service.py # Embedding cache test
|
| 377 |
│
|
| 378 |
+
├── download_assets.py # Downloads DB + snapshots + reranker from HF dataset
|
| 379 |
├── startup.sh # HF Spaces entrypoint
|
| 380 |
├── requirements.txt
|
| 381 |
└── .env / .env.example
|
|
|
|
| 416 |
|
| 417 |
```
|
| 418 |
query → Phase 1: FTS5 (30 results)
|
| 419 |
+
→ Phase 2: Embedding → Qdrant Vector (30 results, jina-embeddings-v5)
|
| 420 |
+
├── Try Ollama (jina-embeddings-v5 GGUF, timeout 5s)
|
| 421 |
+
└── Fallback → sentence-transformers (jina-embeddings-v5 PyTorch)
|
| 422 |
→ Merge & deduplicate by (volume, page)
|
| 423 |
→ Phase 3: ONNX Reranker (jina-reranker-v2)
|
| 424 |
→ Sort by rerank_score
|
|
|
|
| 474 |
- FTS5: `pages_fts` (virtual table on `content_text`, `unicode61` tokenizer)
|
| 475 |
- In-memory: Full DB loaded to `:memory:` via `sqlite3.backup()` at startup
|
| 476 |
- Search log: Disk-persisted for autocomplete (frequency-ranked)
|
| 477 |
+
- File size: ~238MB
|
| 478 |
|
| 479 |
#### Qdrant
|
| 480 |
- Two modes: **Local** (embedded, snapshot restore) or **Server** (remote connection)
|
| 481 |
+
- Collections:
|
| 482 |
+
- `tipitaka_chunks` (scripture chunks, ~41K points, 460MB snapshot)
|
| 483 |
+
- `tipitaka_scripture` (raw scripture, ~25K points, 496MB snapshot)
|
| 484 |
- Autodetect: Checks `localhost:6333` if not configured
|
| 485 |
- Snapshot restore: Extracts `.snapshot` files to storage directory on first run
|
| 486 |
+
- Embeding: **jina-embeddings-v5-small-retrieval** (1024 dims) — dual-mode
|
| 487 |
- LRU cache: 256 entries (OrderedDict) — avoids re-embedding identical queries
|
| 488 |
|
| 489 |
#### ONNX Reranker
|
| 490 |
- Model: **jina-reranker-v2-base-multilingual** (ONNX export, CPU)
|
| 491 |
+
- Location: `models/jina-v2-onnx/` (~267MB)
|
| 492 |
- Fallback: Graceful if unavailable (skips reranking step)
|
| 493 |
|
| 494 |
---
|
| 495 |
|
| 496 |
+
## 5. Dual-Mode Embedding System (NEW in 2.2.0)
|
| 497 |
+
|
| 498 |
+
### 5.1 Motivation
|
| 499 |
+
|
| 500 |
+
ปัญหา: บน local ใช้ Ollama ทำ embedding ได้ แต่ **HF Space ไม่มี Ollama**
|
| 501 |
+
ทางแก้: เพิ่ม sentence-transformers fallback — เขียนใน `_get_embedding()` ของ RAGService
|
| 502 |
+
|
| 503 |
+
### 5.2 Strategy
|
| 504 |
+
|
| 505 |
+
```python
|
| 506 |
+
def _get_embedding(self, text: str) -> list:
|
| 507 |
+
# ✅ Try 1: Ollama (local dev) — timeout 5s
|
| 508 |
+
try:
|
| 509 |
+
response = httpx.post(f"{self.ollama_url}/api/embed", ...)
|
| 510 |
+
return embedding
|
| 511 |
+
except Exception:
|
| 512 |
+
pass # → fallback
|
| 513 |
+
|
| 514 |
+
# ✅ Try 2: sentence-transformers (HF Space / no Ollama)
|
| 515 |
+
if self._st_model is None:
|
| 516 |
+
from sentence_transformers import SentenceTransformer
|
| 517 |
+
self._st_model = SentenceTransformer(
|
| 518 |
+
"jinaai/jina-embeddings-v5-text-small-retrieval"
|
| 519 |
+
)
|
| 520 |
+
return self._st_model.encode(text, normalize_embeddings=True).tolist()
|
| 521 |
+
```
|
| 522 |
+
|
| 523 |
+
### 5.3 Model Details
|
| 524 |
+
|
| 525 |
+
| | Ollama Mode | Sentence-Transformers Mode |
|
| 526 |
+
|---|---|---|
|
| 527 |
+
| **Model** | `jina-embeddings-v5-small-retrieval` (GGUF) | `jina-embeddings-v5-text-small-retrieval` (PyTorch) |
|
| 528 |
+
| **Size** | 596MB (Q6_K) | ~600MB |
|
| 529 |
+
| **Dimensions** | 1024 | 1024 |
|
| 530 |
+
| **Load type** | External process (Ollama) | In-process (Python) |
|
| 531 |
+
| **RAM usage** | ~700MB (Ollama process) | ~1.2GB |
|
| 532 |
+
| **Speed** | ~50ms (network call) | ~100ms (first call slower) |
|
| 533 |
+
| **Cache** | LRU 256 entries — both modes | Shared |
|
| 534 |
|
| 535 |
+
### 5.4 Configuration
|
| 536 |
+
|
| 537 |
+
```python
|
| 538 |
+
# rag_service.py — constants
|
| 539 |
+
OLLAMA_URL = "http://localhost:11434"
|
| 540 |
+
EMBED_MODEL = "hf.co/jinaai/...-v5-small-retrieval-Q6_K.gguf"
|
| 541 |
+
ST_EMBED_MODEL = "jinaai/jina-embeddings-v5-text-small-retrieval"
|
| 542 |
+
EMBED_DIMS = 1024 # ตรวจสอบแล้วว่าทั้งสอง mode ใช้ 1024 dims
|
| 543 |
+
```
|
| 544 |
+
|
| 545 |
+
- `OLLAMA_URL` สามารถ override ได้ผ่าน environment variable
|
| 546 |
+
- ถ้า `sentence-transformers` ก็ไม่ต้อง setup อะไรเพิ่ม — โหลด model auto จาก HF Hub
|
| 547 |
+
|
| 548 |
+
---
|
| 549 |
+
|
| 550 |
+
## 6. Deployment
|
| 551 |
+
|
| 552 |
+
### 6.1 Hugging Face Spaces
|
| 553 |
|
| 554 |
```dockerfile
|
| 555 |
# Dockerfile — multi-stage build
|
|
|
|
| 561 |
```
|
| 562 |
|
| 563 |
**startup.sh** downloads:
|
| 564 |
+
- `tipitaka_mcu.db` (SQLite corpus, ~238MB)
|
| 565 |
+
- `qdrant/tipitaka_chunks.snapshot` (vector index, ~460MB)
|
| 566 |
+
- `qdrant/tipitaka_scripture.snapshot` (vector index, ~496MB)
|
| 567 |
+
- ONNX reranker via huggingface_hub (~267MB)
|
| 568 |
+
|
| 569 |
+
ทั้งหมดดึงมาจาก dataset: `dhammawatthumpra/tipitaka-storage`
|
| 570 |
|
| 571 |
**Qdrant lock cleanup:** ก่อน start uvicorn, startup.sh ลบ Qdrant lock file ที่ค้างจาก session ก่อนหน้า:
|
| 572 |
```bash
|
|
|
|
| 574 |
```
|
| 575 |
ป้องกัน error `"already accessed by another instance"` หลัง HF Space restart
|
| 576 |
|
| 577 |
+
### 6.2 Environment Variables (.env)
|
| 578 |
|
| 579 |
| Variable | Default | Description |
|
| 580 |
|----------|---------|-------------|
|
|
|
|
| 587 |
| `QDRANT_PATH` | Auto-detect | Qdrant storage path |
|
| 588 |
| `QDRANT_URL` | `None` | Remote Qdrant URL (optional) |
|
| 589 |
| `SNAPSHOT_DIR` | Auto-detect | Snapshot restore directory |
|
| 590 |
+
| `OLLAMA_URL` | `http://localhost:11434` | Ollama endpoint (override for HF/docker) |
|
| 591 |
| `CORS_ORIGINS` | `"*"` | CORS allowed origins |
|
| 592 |
| `SERVE_STATIC` | `false` | Serve built frontend (HF Spaces) |
|
| 593 |
| `PORT` | `8000` | Server port |
|
| 594 |
| `DEBUG` | `true` | Debug mode |
|
| 595 |
|
| 596 |
+
### 6.3 Path Auto-Detection
|
| 597 |
|
| 598 |
Config intelligently resolves paths based on environment:
|
| 599 |
|
|
|
|
| 606 |
|
| 607 |
---
|
| 608 |
|
| 609 |
+
## 7. Key Design Decisions
|
| 610 |
|
| 611 |
+
### 7.1 Why Qdrant over ChromaDB?
|
| 612 |
- **Persistence**: Qdrant stores on disk reliably; ChromaDB had data-loss issues
|
| 613 |
- **Dual mode**: Local (embedded, no Docker) + Server (production) — same API
|
| 614 |
- **Snapshot restore**: `.snapshot` files enable deployment without reindexing
|
| 615 |
- **Performance**: Faster vector search with tunable `score_threshold`
|
| 616 |
|
| 617 |
+
### 7.2 Why jina-embeddings-v5 over jina-embeddings-v3?
|
| 618 |
+
- **Newer model**: v5-small-retrieval (Oct 2024) vs v3 (Mar 2024)
|
| 619 |
+
- **1024 dims เหมือนกัน**: ไม่ต้อง re-index Qdrant
|
| 620 |
+
- **Thai/Pali accuracy**: Confirmed same dimension space, better retrieval quality
|
| 621 |
+
- **ยังคงมี GGUF ผ่าน Ollama**: สำหรับ local dev
|
| 622 |
+
- **v5 ยังมี PyTorch บน HF Hub**: `jinaai/jina-embeddings-v5-text-small-retrieval` ใช้กับ sentence-transformers ได้
|
| 623 |
+
|
| 624 |
+
### 7.3 Why sentence-transformers Fallback?
|
| 625 |
+
- **HF Space ไม่มี Ollama**: ต้องมี embedding source อื่น
|
| 626 |
+
- **No extra dependencies**: `sentence-transformers>=3.0` + `torch>=2.0` มีใน requirements.txt อยู่แล้ว
|
| 627 |
+
- **One model, two runtimes**: ใช้ jina-embeddings-v5 ทั้งใน Ollama (GGUF) และ PyTorch — vector space เดียวกัน
|
| 628 |
+
- **Seamless fallback**: ถ้า Ollama ไม่ว่าง → ใช้ ST โดยอัตโนมัติ, user ไม่รู้สึก
|
| 629 |
+
- **Benchmark**: Ollama OFF (15.9s) vs Ollama ON (22.3s) — fallback เร็วกว่าเพราะ in-process
|
| 630 |
+
|
| 631 |
+
### 7.4 Why Page-by-Page over Continuous Scroll?
|
| 632 |
- **Simplicity**: Avoids virtual scrolling complexity
|
| 633 |
- **Performance**: One page loaded at a time (no memory buildup)
|
| 634 |
- **Navigation**: Clear page markers, prev/next predictable
|
| 635 |
- **Swipe**: Mobile gesture ← → works naturally with page boundaries
|
| 636 |
|
| 637 |
+
### 7.5 Why No PWA/Offline?
|
| 638 |
- **Content is dynamic**: Pages rendered with AI context and search results
|
| 639 |
- **Token-based auth**: Not applicable (public content, but still server-dependant)
|
| 640 |
- **Snapshot data too large**: 250MB+ not practical for offline storage
|
| 641 |
|
| 642 |
+
### 7.6 Why Split First Page at "ขอนอบน้อม"?
|
| 643 |
- **Universal**: Works for all 45 volumes — the homage line is the only consistent structural element on page 1
|
| 644 |
- **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
|
| 645 |
- **Visual hierarchy**: Homage belongs to the top "ceremonial" zone (centered, clean); actual content starts below with normal styling
|
| 646 |
|
| 647 |
+
### 7.7 Why text-justify for Body Text?
|
| 648 |
- **Reading experience**: Justified text mimics printed Tipitaka books, creating a familiar sacred-text feel
|
| 649 |
- **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)
|
| 650 |
- **User preference**: ผู้ใช้ request ให้จัด justify (2026-05-07)
|
| 651 |
|
| 652 |
---
|
| 653 |
|
| 654 |
+
## 8. Test Coverage
|
| 655 |
|
| 656 |
| Module | Tests | What it covers |
|
| 657 |
|--------|-------|----------------|
|