import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useUIStore, useReaderStore, useThemeStore } from '../../stores/appStore'; import { useSearchStore } from '../../stores/searchStore'; import type { SearchMode } from '../../stores/searchStore'; import api from '../../lib/api'; import { X, Book, Search, BarChart2, ChevronRight, Loader2, ChevronDown, Zap, Sparkles } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; // NavDrawer bg matches Shell bg per theme — seamless sidebar // Dark: bg=#1a1a2e text=#e0e0e0 border=#3a3a5e // Light: bg=#f8f6f0 text=#1a1a1a border=#e0d0b0 // Classic:bg=#f5edd8 text=#1a1a1a border=#d4c4a0 const DRAWER_STYLES: Record = { dark: { bg: 'bg-[#1a1a2e]', text: 'text-[#e0e0e0]', border: 'border-[#3a3a5e]', tab: 'bg-[#222242]', input: 'bg-[#222242]' }, light: { bg: 'bg-[#f8f6f0]', text: 'text-[#1a1a1a]', border: 'border-[#e0d0b0]', tab: 'bg-[#fdfaf5]', input: 'bg-white' }, classic: { bg: 'bg-[#f5edd8]', text: 'text-[#1a1a1a]', border: 'border-[#d4c4a0]', tab: 'bg-[#fdfaf5]', input: 'bg-white' }, }; /** Helper to extract all highlighted terms inside tags from search snippets, * so we can highlight individual matched words in the reader page. * Falls back to the original query if no marks are found. */ const getHighlightQuery = (originalQuery: string, results: any[]): string => { const terms = new Set(); const regex = /(.*?)<\/mark>/gi; results.forEach(res => { if (res.snippet) { let match; regex.lastIndex = 0; while ((match = regex.exec(res.snippet)) !== null) { const term = match[1].replace(/<[^>]+>/g, '').trim(); // clean any nested tags if (term.length > 1) { terms.add(term); } } } }); if (terms.size > 0) { return Array.from(terms).join(' '); } return originalQuery; }; const NavDrawer: React.FC = () => { const { isNavOpen, setNavOpen, activeTab, setActiveTab } = useUIStore(); const { currentVolume, setVolume, setPage, setHighlightQuery } = useReaderStore(); const { theme } = useThemeStore(); const { query, setQuery, results, totalResults, isLoading, isAiLoading, performSearch, performAiSearch, breakdown, timeTaken, searchMode, preferredMode, setPreferredMode, suggestions, showSuggestions, fetchSuggestions, setShowSuggestions, highlightWords, } = useSearchStore(); const [modeDropdownOpen, setModeDropdownOpen] = useState(false); const modeDropdownRef = useRef(null); const [expandedVols, setExpandedVols] = useState>({}); const toggleVolExpand = (volId: number) => { setExpandedVols(prev => ({ ...prev, [volId]: !prev[volId] })); }; const [toc, setToc] = useState([]); const [loadingToc, setLoadingToc] = useState(false); const [volumes, setVolumes] = useState([]); const [activeSuggestion, setActiveSuggestion] = useState(-1); const searchRef = useRef(null); const debounceRef = useRef | null>(null); const getHighlightString = () => { if (highlightWords && highlightWords.length > 0) { return highlightWords.join(' '); } return getHighlightQuery(query, results); }; const s = DRAWER_STYLES[theme] ?? DRAWER_STYLES.dark; useEffect(() => { api.get('/api/pages/volumes').then(res => setVolumes(res.data)); }, []); useEffect(() => { if (activeTab !== 'toc') return; setLoadingToc(true); // volume_number = currentVolume → matches WHERE v.volume_number = ? api.get(`/api/pages/volumes/${currentVolume}/toc`) .then(res => setToc(res.data)) .finally(() => setLoadingToc(false)); }, [activeTab, currentVolume]); const jumpToPage = (volumeNumber: number, pageNum: number, highlight?: string) => { if (window.innerWidth < 1024) { setNavOpen(false); setTimeout(() => { setVolume(volumeNumber); setPage(pageNum); setHighlightQuery(highlight ?? ''); }, 250); } else { setVolume(volumeNumber); setPage(pageNum); setHighlightQuery(highlight ?? ''); } }; // Debounced suggestion fetch const handleQueryChange = useCallback((value: string) => { setQuery(value); setActiveSuggestion(-1); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { fetchSuggestions(value); }, 280); }, [setQuery, fetchSuggestions]); // Close suggestions + mode dropdown on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (searchRef.current && !searchRef.current.contains(e.target as Node)) { setShowSuggestions(false); } if (modeDropdownRef.current && !modeDropdownRef.current.contains(e.target as Node)) { setModeDropdownOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [setShowSuggestions]); const handleModeSelect = (mode: SearchMode) => { setPreferredMode(mode); setModeDropdownOpen(false); }; const selectSuggestion = (term: string) => { setQuery(term); setShowSuggestions(false); setActiveSuggestion(-1); setActiveTab('search'); setTimeout(() => performSearch(), 0); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestions || suggestions.length === 0) { if (e.key === 'Enter') { e.preventDefault(); setActiveTab('search'); performSearch(); } return; } if (e.key === 'ArrowDown') { e.preventDefault(); setActiveSuggestion(i => Math.min(i + 1, suggestions.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveSuggestion(i => Math.max(i - 1, -1)); } else if (e.key === 'Enter') { e.preventDefault(); if (activeSuggestion >= 0) { selectSuggestion(suggestions[activeSuggestion]); } else { setShowSuggestions(false); setActiveTab('search'); performSearch(); } } else if (e.key === 'Escape') { setShowSuggestions(false); setActiveSuggestion(-1); } }; const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setShowSuggestions(false); setActiveTab('search'); performSearch(); }; const tabs = [ { id: 'toc', icon: , label: 'สารบัญ' }, { id: 'search', icon: , label: 'ค้นหา' }, { id: 'overview', icon: , label: 'ภาพรวม' }, ] as const; return ( <> {/* Mobile backdrop */} {isNavOpen && ( setNavOpen(false)} className="fixed inset-0 bg-black/50 z-40 lg:hidden" /> )} {/* Drawer */} {/* Header */}
พระไตรปิฎก ฉบับมหาจุฬาลงกรณราชวิทยาลัย
{/* X button — ทุก screen size (ไม่ใช่แค่ mobile) */}
{/* Tab bar */}
{tabs.map(tab => ( ))}
{/* Content */}
{/* ── TOC ── */} {activeTab === 'toc' && (
{loadingToc ? (
) : (
{toc.map((item, i) => ( ))}
)}
)} {/* ── Search ── */} {activeTab === 'search' && (
{/* Search input */} handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onFocus={() => query && fetchSuggestions(query)} placeholder="ค้นหาข้อความ เช่น อริยสัจ…" autoComplete="off" 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`} /> {isLoading && } {/* Suggestions dropdown */} {showSuggestions && suggestions.length > 0 && ( {suggestions.map((term, i) => (
  • ))}
    )}
    {/* Split search button with mode dropdown */}
    {/* Main search button */} {/* Mode dropdown toggle */}
    {modeDropdownOpen && (
    )}
    {results.length > 0 && ( <> {/* Result count + mode badge */}

    พบ {totalResults} หน้า ({timeTaken.toFixed(0)}ms)

    {searchMode === 'ai' ? '✦ AI' : '⚡ FTS'}
    {/* Volume breakdown bars — แสดงทุกเล่มที่พบ ไม่จำกัดจำนวน */} {Object.keys(breakdown).length > 1 && (

    การกระจายตัว

    {Object.entries(breakdown) .sort((a, b) => Number(a[0]) - Number(b[0])) .map(([volId, stats]) => { const numericVolId = Number(volId); const vol = volumes.find(v => v.id === numericVolId); const maxCount = Math.max(...Object.values(breakdown).map(s => s.count)); const pct = (stats.count / maxCount) * 100; const isExpanded = !!expandedVols[numericVolId]; const hasPages = stats.pages && stats.pages.length > 0; return (
    {/* Header Bar Area (คลิกเพื่อขยาย) */}
    toggleVolExpand(numericVolId)} className="w-full text-left space-y-0.5 cursor-pointer group/bar" >
    เล่ม {vol?.volume_number} — {vol?.title} {stats.count} {hasPages && ( )}
    {/* Expanded Page Chips */} {isExpanded && hasPages && (
    {stats.pages?.map((pageNum) => ( ))}
    )}
    ); })}
    )} {/* Result cards */}
    {results.map((res, i) => { const vol = volumes.find(v => v.id === res.volume_id); const volNum = res.volume_number ?? vol?.volume_number; return (
    {/* Google-style breadcrumb */}
    {/* เล่ม N */} {/* › พระสูตร/หัวข้อ */} {res.toc_title && ( <> )} {/* › หน้า N */} <>
    {/* Snippet — กดทั้ง card ไปหน้าที่พบ */}
    ); })}
    {/* AI upgrade button — แสดงเฉพาะเมื่อผลเป็น FTS และ preferredMode ไม่ใช่ ai */} {searchMode === 'fts' && ( )} )} {!isLoading && results.length === 0 && query && (

    ไม่พบ «{query}»

    {searchMode === 'fts' && (
    )}
    )}
    )} {/* ── Overview ── */} {activeTab === 'overview' && (

    พระไตรปิฎก AI Expert

    โครงการสืบค้นพระไตรปิฎก ฉบับมหาจุฬาลงกรณราชวิทยาลัย ด้วย AI และ Semantic Search

    เทคโนโลยีที่ใช้

    {['FastAPI + Python', 'Vite + React', 'SQLite FTS5', 'Turbovec + Reranker'].map(t => (
    {t}
    ))}

    แหล่งข้อมูลอ้างอิง

    พระไตรปิฎกฉบับมหาจุฬาลงกรณราชวิทยาลัย อ้างอิงต้นฉบับดิจิทัลและเผยแผ่โดยมีเว็บไซต์{' '} 84000.org {' '} เป็นแหล่งข้อมูลปฐมภูมิ (Primary Data Source) ของโครงการ

    ข้อสงวนสิทธิ์ (Disclaimer): “84000.org เป็นผู้เอื้อเฟื้อข้อมูลต้นฉบับ แต่ไม่มีส่วนรับผิดชอบต่อผลลัพธ์ที่เกิดจากการประมวลผลของระบบ AI”

    เข้าชมเว็บไซต์ต้นฉบับ
    )}
    ); }; export default NavDrawer;