Spaces:
Running
Running
Commit ·
c9166a1
1
Parent(s): 17f10f6
feat: use contents table for full section hierarchy in reader (sections array instead of single title)
Browse files
webapp/tipitaka-api/app/services/page_service.py
CHANGED
|
@@ -26,11 +26,7 @@ class PageService:
|
|
| 26 |
cursor.execute("""
|
| 27 |
SELECT p.id, p.volume_id, v.volume_number, p.page_number,
|
| 28 |
p.content_text, p.content_html, v.title,
|
| 29 |
-
v.total_pages
|
| 30 |
-
(SELECT c2.title FROM contents c2
|
| 31 |
-
WHERE c2.volume_id = p.volume_id
|
| 32 |
-
AND c2.page_number <= p.page_number
|
| 33 |
-
ORDER BY c2.page_number DESC LIMIT 1) AS section_title
|
| 34 |
FROM pages p
|
| 35 |
JOIN volumes v ON p.volume_id = v.id
|
| 36 |
WHERE v.volume_number = ? AND p.page_number = ?
|
|
@@ -40,6 +36,22 @@ class PageService:
|
|
| 40 |
if not row:
|
| 41 |
return None
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
cleaned = self.search_service.clean_text(row["content_text"])
|
| 44 |
|
| 45 |
# If page has less than 50 chars, return blank content
|
|
@@ -53,7 +65,7 @@ class PageService:
|
|
| 53 |
"content_text": "" if is_blank else cleaned,
|
| 54 |
"content_html": "" if is_blank else (row["content_html"] or ""),
|
| 55 |
"title": row["title"],
|
| 56 |
-
"
|
| 57 |
"is_blank": is_blank,
|
| 58 |
}
|
| 59 |
|
|
|
|
| 26 |
cursor.execute("""
|
| 27 |
SELECT p.id, p.volume_id, v.volume_number, p.page_number,
|
| 28 |
p.content_text, p.content_html, v.title,
|
| 29 |
+
v.total_pages
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
FROM pages p
|
| 31 |
JOIN volumes v ON p.volume_id = v.id
|
| 32 |
WHERE v.volume_number = ? AND p.page_number = ?
|
|
|
|
| 36 |
if not row:
|
| 37 |
return None
|
| 38 |
|
| 39 |
+
# Get all section headings for this page (including parent pages)
|
| 40 |
+
cursor.execute("""
|
| 41 |
+
SELECT DISTINCT c.title, c.level
|
| 42 |
+
FROM contents c
|
| 43 |
+
WHERE c.volume_id = ? AND c.page_number <= ?
|
| 44 |
+
AND c.level IN (0, 1)
|
| 45 |
+
AND c.title NOT LIKE '%---%'
|
| 46 |
+
AND c.title NOT LIKE '%หน้าว่าง%'
|
| 47 |
+
ORDER BY c.page_number, c.level, c.id
|
| 48 |
+
""", (row["volume_id"], page_number))
|
| 49 |
+
sections = [
|
| 50 |
+
{"title": r["title"].strip(), "level": r["level"]}
|
| 51 |
+
for r in cursor.fetchall()
|
| 52 |
+
if r["title"].strip()
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
cleaned = self.search_service.clean_text(row["content_text"])
|
| 56 |
|
| 57 |
# If page has less than 50 chars, return blank content
|
|
|
|
| 65 |
"content_text": "" if is_blank else cleaned,
|
| 66 |
"content_html": "" if is_blank else (row["content_html"] or ""),
|
| 67 |
"title": row["title"],
|
| 68 |
+
"sections": sections,
|
| 69 |
"is_blank": is_blank,
|
| 70 |
}
|
| 71 |
|
webapp/tipitaka-web/src/components/layout/ReaderPanel.tsx
CHANGED
|
@@ -19,17 +19,31 @@ const SHELL_STYLES: Record<string, string> = {
|
|
| 19 |
};
|
| 20 |
|
| 21 |
// Split on double-newline → paragraphs; preserve single newlines as <br/>
|
| 22 |
-
const renderContent = (text: string, fontSize: number,
|
| 23 |
const paragraphs = text.split(/\n\n+/).filter(Boolean);
|
| 24 |
return (
|
| 25 |
<div
|
| 26 |
style={{ fontSize: `${fontSize}px`, lineHeight: '2.1' }}
|
| 27 |
className="reader-content text-left space-y-5"
|
| 28 |
>
|
| 29 |
-
{
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
)}
|
| 34 |
{paragraphs.map((para, idx) => {
|
| 35 |
// Detect sub-headings: ends with colon, or story-heading pattern like "เรื่องXXX ๑ เรื่อง"
|
|
@@ -65,7 +79,7 @@ const ReaderPanel: React.FC = () => {
|
|
| 65 |
const {
|
| 66 |
currentVolume, currentPage,
|
| 67 |
setCurrentContent, setTotalPages,
|
| 68 |
-
|
| 69 |
} = useReaderStore();
|
| 70 |
|
| 71 |
const { theme, fontSize } = useThemeStore();
|
|
@@ -86,7 +100,6 @@ const ReaderPanel: React.FC = () => {
|
|
| 86 |
setContent(data);
|
| 87 |
setCurrentContent(data.content_text || '');
|
| 88 |
if (data.total_pages) setTotalPages(data.total_pages);
|
| 89 |
-
if (data.section_title) setSectionTitle(data.section_title);
|
| 90 |
if (data.title) setVolumeTitle(data.title);
|
| 91 |
})
|
| 92 |
.catch(err => console.error('Page load error:', err))
|
|
@@ -129,7 +142,7 @@ const ReaderPanel: React.FC = () => {
|
|
| 129 |
</p>
|
| 130 |
</header>
|
| 131 |
|
| 132 |
-
{renderContent(content.content_text || '', fontSize, content.
|
| 133 |
|
| 134 |
<footer className="mt-12 pt-6 border-t border-[#888]/10 text-center">
|
| 135 |
<span className="text-xs text-[#888]">
|
|
|
|
| 19 |
};
|
| 20 |
|
| 21 |
// Split on double-newline → paragraphs; preserve single newlines as <br/>
|
| 22 |
+
const renderContent = (text: string, fontSize: number, sections: {title: string; level: number}[]) => {
|
| 23 |
const paragraphs = text.split(/\n\n+/).filter(Boolean);
|
| 24 |
return (
|
| 25 |
<div
|
| 26 |
style={{ fontSize: `${fontSize}px`, lineHeight: '2.1' }}
|
| 27 |
className="reader-content text-left space-y-5"
|
| 28 |
>
|
| 29 |
+
{/* Section hierarchy breadcrumb */}
|
| 30 |
+
{sections.length > 0 && (
|
| 31 |
+
<div className="mb-6 space-y-1">
|
| 32 |
+
{sections.map((sec, i) => (
|
| 33 |
+
<div key={i} className={sec.level === 0 ? '' : 'ml-4'}>
|
| 34 |
+
{sec.level === 0 ? (
|
| 35 |
+
<h2 className="text-[#c8860a] text-xl md:text-2xl font-bold leading-snug">
|
| 36 |
+
{sec.title}
|
| 37 |
+
</h2>
|
| 38 |
+
) : (
|
| 39 |
+
<h3 className="text-[#c8860a]/80 text-base md:text-lg font-semibold leading-snug mt-1">
|
| 40 |
+
{sec.title}
|
| 41 |
+
</h3>
|
| 42 |
+
)}
|
| 43 |
+
</div>
|
| 44 |
+
))}
|
| 45 |
+
<div className="border-b border-[#c8860a]/20 mt-3" />
|
| 46 |
+
</div>
|
| 47 |
)}
|
| 48 |
{paragraphs.map((para, idx) => {
|
| 49 |
// Detect sub-headings: ends with colon, or story-heading pattern like "เรื่องXXX ๑ เรื่อง"
|
|
|
|
| 79 |
const {
|
| 80 |
currentVolume, currentPage,
|
| 81 |
setCurrentContent, setTotalPages,
|
| 82 |
+
setVolumeTitle,
|
| 83 |
} = useReaderStore();
|
| 84 |
|
| 85 |
const { theme, fontSize } = useThemeStore();
|
|
|
|
| 100 |
setContent(data);
|
| 101 |
setCurrentContent(data.content_text || '');
|
| 102 |
if (data.total_pages) setTotalPages(data.total_pages);
|
|
|
|
| 103 |
if (data.title) setVolumeTitle(data.title);
|
| 104 |
})
|
| 105 |
.catch(err => console.error('Page load error:', err))
|
|
|
|
| 142 |
</p>
|
| 143 |
</header>
|
| 144 |
|
| 145 |
+
{renderContent(content.content_text || '', fontSize, content.sections || [])}
|
| 146 |
|
| 147 |
<footer className="mt-12 pt-6 border-t border-[#888]/10 text-center">
|
| 148 |
<span className="text-xs text-[#888]">
|