dhammawatthumpra commited on
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
- "section_title": row["section_title"] or "",
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, sectionTitle: string) => {
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
- {sectionTitle && (
30
- <h2 className="text-[#c8860a] text-xl md:text-2xl font-bold mb-6 leading-snug border-b border-[#c8860a]/20 pb-3">
31
- {sectionTitle}
32
- </h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- setSectionTitle, setVolumeTitle,
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.section_title)}
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]">