dhammawatthumpra commited on
Commit
23b4d13
·
1 Parent(s): b1f23a6

feat: enhance footnote extraction and display at bottom of page

Browse files
webapp/tipitaka-api/app/services/page_service.py CHANGED
@@ -1,4 +1,5 @@
1
  import re
 
2
  from app.database.sqlite_db import SQLiteDB
3
  from app.services.search_service import SearchService
4
 
@@ -273,14 +274,44 @@ class PageService:
273
  cleaned = self.search_service.clean_text(row["content_text"])
274
  is_blank = self._is_blank_page(cleaned)
275
 
276
- # Extract end markers from raw content_html: <B> tags containing "จบ"
277
  raw_html = row["content_html"] or ""
278
  end_markers = []
 
 
279
  if not is_blank and raw_html:
 
280
  for m in re.finditer(r'<B>(.*?จบ.*?)</B>', raw_html, re.IGNORECASE):
281
  tm = m.group(1).strip()
282
  if tm and tm not in end_markers:
283
  end_markers.append(tm)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  return {
286
  "id": row["id"],
@@ -294,6 +325,7 @@ class PageService:
294
  "title": row["title"],
295
  "sections": sections,
296
  "end_markers": end_markers,
 
297
  "is_blank": is_blank,
298
  }
299
 
@@ -366,14 +398,17 @@ class PageService:
366
 
367
  for line in lines:
368
  line_strip = line.strip()
 
 
 
369
  # Definition start pattern: matches @ followed by optional bracket/paren and the id
370
  # e.g. @๑, @(๑), @[๑], @๑-
371
  start_pattern = rf'^@\s*[(\[]?{re.escape(clean_id)}[)\]-]?.*'
372
 
373
- if re.match(start_pattern, line_strip):
374
  capturing = True
375
- # Strip the @ and marker part
376
- content = re.sub(rf'^@\s*[(\[]?{re.escape(clean_id)}[)\]-]?\s*', '', line_strip)
377
  if content:
378
  found_lines.append(content)
379
  elif capturing:
 
1
  import re
2
+ from typing import Optional
3
  from app.database.sqlite_db import SQLiteDB
4
  from app.services.search_service import SearchService
5
 
 
274
  cleaned = self.search_service.clean_text(row["content_text"])
275
  is_blank = self._is_blank_page(cleaned)
276
 
277
+ # Extract end markers and footnotes from raw content_html
278
  raw_html = row["content_html"] or ""
279
  end_markers = []
280
+ footnotes = []
281
+
282
  if not is_blank and raw_html:
283
+ # End markers
284
  for m in re.finditer(r'<B>(.*?จบ.*?)</B>', raw_html, re.IGNORECASE):
285
  tm = m.group(1).strip()
286
  if tm and tm not in end_markers:
287
  end_markers.append(tm)
288
+
289
+ # All Footnotes
290
+ # Strip line numbers first as they might precede the @ on the same line
291
+ fn_html = re.sub(r'<span\s+class="LineNumber">.*?</span>', '', raw_html)
292
+ lines = fn_html.split('\n')
293
+ current_fn = None
294
+ for line in lines:
295
+ line_strip = line.strip()
296
+ if not line_strip.startswith('@'):
297
+ continue
298
+
299
+ line_plain = re.sub(r'<[^>]+>', '', line_strip)
300
+ # Match @ marker content
301
+ # e.g. @๑ เนื้อหา... or @(๑) เนื้อหา...
302
+ m = re.match(r'^@\s*([(\[]?[\u0E50-\u0E59\d]+[)\]-]?)\s*(.*)', line_plain)
303
+ if m:
304
+ marker = m.group(1).strip()
305
+ text = m.group(2).strip()
306
+ # Clean marker for lookup: [๑] -> ๑
307
+ clean_marker = re.sub(r'[()\[\]-]', '', marker).strip()
308
+
309
+ if clean_marker and not re.search(r'เชิงอรรถ', marker):
310
+ current_fn = {"id": marker, "content": text}
311
+ footnotes.append(current_fn)
312
+ elif current_fn:
313
+ # Continuation line starting with @
314
+ current_fn["content"] += " " + line_plain.lstrip('@').strip()
315
 
316
  return {
317
  "id": row["id"],
 
325
  "title": row["title"],
326
  "sections": sections,
327
  "end_markers": end_markers,
328
+ "footnotes": footnotes,
329
  "is_blank": is_blank,
330
  }
331
 
 
398
 
399
  for line in lines:
400
  line_strip = line.strip()
401
+ # Remove tags for matching to handle @<I>๑</I>
402
+ line_plain = re.sub(r'<[^>]+>', '', line_strip)
403
+
404
  # Definition start pattern: matches @ followed by optional bracket/paren and the id
405
  # e.g. @๑, @(๑), @[๑], @๑-
406
  start_pattern = rf'^@\s*[(\[]?{re.escape(clean_id)}[)\]-]?.*'
407
 
408
+ if re.match(start_pattern, line_plain):
409
  capturing = True
410
+ # Strip the @ and marker part from the plain text to get content
411
+ content = re.sub(rf'^@\s*[(\[]?{re.escape(clean_id)}[)\]-]?\s*', '', line_plain)
412
  if content:
413
  found_lines.append(content)
414
  elif capturing:
webapp/tipitaka-web/src/components/layout/ReaderPanel.tsx CHANGED
@@ -152,7 +152,9 @@ const ReaderPanel: React.FC = () => {
152
  x: rect.left + rect.width / 2,
153
  y: rect.bottom,
154
  id: target.innerText,
155
- type: isAbbrev ? 'abbrev' : 'footnote'
 
 
156
  });
157
  }
158
  }
@@ -210,6 +212,26 @@ const ReaderPanel: React.FC = () => {
210
  </>
211
  )}
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  <footer className="mt-12 pt-6 border-t border-[#888]/10 text-center">
214
  <span className="text-xs text-[#888]">
215
  — เล่ม {currentVolume} หน้า {currentPage} —
 
152
  x: rect.left + rect.width / 2,
153
  y: rect.bottom,
154
  id: target.innerText,
155
+ type: isAbbrev ? 'abbrev' : 'footnote',
156
+ vol: currentVolume,
157
+ page: currentPage
158
  });
159
  }
160
  }
 
212
  </>
213
  )}
214
 
215
+ {/* Footnotes Section */}
216
+ {content.footnotes && content.footnotes.length > 0 && (
217
+ <div className="mt-16 pt-8 border-t border-[#c8860a]/20">
218
+ <h5 className="text-[#c8860a] text-xs font-bold tracking-widest uppercase mb-6 flex items-center gap-2">
219
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
220
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 4v12l-4-2-4 2V4M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
221
+ </svg>
222
+ เชิงอรรถ (Footnotes)
223
+ </h5>
224
+ <div className="space-y-4">
225
+ {content.footnotes.map((fn: any, idx: number) => (
226
+ <div key={idx} className="flex gap-3 text-[13px] leading-relaxed text-white/50 hover:text-white/80 transition-colors">
227
+ <span className="text-[#c8860a] font-bold min-w-[24px] text-right shrink-0">{fn.id}</span>
228
+ <span className="font-light">{fn.content}</span>
229
+ </div>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ )}
234
+
235
  <footer className="mt-12 pt-6 border-t border-[#888]/10 text-center">
236
  <span className="text-xs text-[#888]">
237
  — เล่ม {currentVolume} หน้า {currentPage} —
webapp/tipitaka-web/src/components/reader/ReferencePopup.tsx CHANGED
@@ -6,6 +6,8 @@ export interface RefPopupPos {
6
  y: number;
7
  id: string;
8
  type: 'abbrev' | 'footnote';
 
 
9
  }
10
 
11
  interface Props {
@@ -79,36 +81,62 @@ const ReferencePopup: React.FC<Props> = ({ pos, onClose }) => {
79
  position: 'fixed',
80
  left: pos.x,
81
  top: pos.y,
82
- transform: 'translate(-50%, 8px)', // 8px gap below the target
83
  zIndex: 9998,
84
  }}
85
- className="w-72 p-4 rounded-xl shadow-2xl bg-[#1a1a2e] border border-[#3a3a5e] text-[#e8e4da] text-sm leading-relaxed"
86
  >
87
- <div className="flex items-center gap-2 mb-2 pb-2 border-b border-[#3a3a5e]">
88
- <span className="text-[#c8860a] font-bold">{pos.id}</span>
89
- <span className="text-xs text-[#888]">
90
- {pos.type === 'abbrev' ? 'ขยายความส่วนที่ย่อ' : 'เชิงอรรถ'}
91
- </span>
92
- {isLoading && <span className="ml-auto animate-pulse text-[10px]">กำลังโหลด...</span>}
93
- </div>
94
 
95
- <div className="max-h-48 overflow-y-auto custom-scrollbar">
96
- {isLoading ? (
97
- <div className="space-y-2 py-1">
98
- <div className="h-3 bg-white/5 rounded w-full animate-pulse" />
99
- <div className="h-3 bg-white/5 rounded w-5/6 animate-pulse" />
100
- <div className="h-3 bg-white/5 rounded w-4/6 animate-pulse" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
- ) : (
103
- <p className="text-[#e8e4da]/90">
104
- {content || "ไม่พบข้อมูลอ้างอิง"}
105
- </p>
106
  )}
107
  </div>
108
- {/* tail arrow pointing up */}
 
109
  <div
110
- className="pointer-events-none absolute left-1/2 -translate-x-1/2 -top-[6px]
111
- w-3 h-3 bg-[#1a1a2e] border-l border-t border-[#3a3a5e] rotate-45"
112
  />
113
  </motion.div>
114
  )}
 
6
  y: number;
7
  id: string;
8
  type: 'abbrev' | 'footnote';
9
+ vol: number;
10
+ page: number;
11
  }
12
 
13
  interface Props {
 
81
  position: 'fixed',
82
  left: pos.x,
83
  top: pos.y,
84
+ transform: 'translate(-50%, 8px)',
85
  zIndex: 9998,
86
  }}
87
+ className="w-80 rounded-2xl shadow-[0_20px_50px_rgba(0,0,0,0.5)] bg-[#1a1a2e]/90 backdrop-blur-xl border border-white/10 text-[#e8e4da] overflow-hidden"
88
  >
89
+ {/* Gradient top bar */}
90
+ <div className="h-1 bg-gradient-to-r from-[#c8860a]/0 via-[#c8860a] to-[#c8860a]/0" />
 
 
 
 
 
91
 
92
+ <div className="p-4">
93
+ <div className="flex items-center justify-between mb-3 pb-2 border-b border-white/5">
94
+ <div className="flex items-center gap-2">
95
+ <span className="bg-[#c8860a] text-[#1a1a2e] text-xs font-bold px-1.5 py-0.5 rounded">
96
+ {pos.id}
97
+ </span>
98
+ <span className="text-xs font-medium text-white/50 tracking-wide uppercase">
99
+ {pos.type === 'abbrev' ? 'Abbreviation' : 'Footnote'}
100
+ </span>
101
+ </div>
102
+ <button
103
+ onClick={onClose}
104
+ className="text-white/30 hover:text-white/70 transition-colors"
105
+ >
106
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
107
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
108
+ </svg>
109
+ </button>
110
+ </div>
111
+
112
+ <div className="max-h-60 overflow-y-auto custom-scrollbar pr-1">
113
+ {isLoading ? (
114
+ <div className="space-y-3 py-1">
115
+ <div className="h-3 bg-white/10 rounded-full w-full animate-pulse" />
116
+ <div className="h-3 bg-white/10 rounded-full w-5/6 animate-pulse" />
117
+ <div className="h-3 bg-white/10 rounded-full w-4/6 animate-pulse" />
118
+ </div>
119
+ ) : (
120
+ <div className="text-[15px] leading-relaxed font-light text-white/90">
121
+ {content || (
122
+ <span className="text-white/40 italic">ไม่พบข้อมูลอ้างอิง</span>
123
+ )}
124
+ </div>
125
+ )}
126
+ </div>
127
+
128
+ {pos.type === 'abbrev' && !isLoading && (
129
+ <div className="mt-3 pt-2 border-t border-white/5 flex items-center gap-1.5">
130
+ <div className="w-1.5 h-1.5 rounded-full bg-[#c8860a] animate-pulse" />
131
+ <span className="text-[10px] text-white/30 uppercase tracking-tighter">AI Expanded</span>
132
  </div>
 
 
 
 
133
  )}
134
  </div>
135
+
136
+ {/* Tail arrow */}
137
  <div
138
+ className="pointer-events-none absolute left-1/2 -translate-x-1/2 -top-[5px]
139
+ w-2.5 h-2.5 bg-[#1a1a2e] border-l border-t border-white/10 rotate-45"
140
  />
141
  </motion.div>
142
  )}