fix(content): book extraction now works when film tags are present

When the user explicitly asks about books, don't bail on pattern
extraction just because the AI also referenced films from the library.
Also fix book pattern regex to match **Title** — *Author* format,
strip sources section before extraction, and remove duplicate title
below BookGrid cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 07:03:29 +00:00
co-authored by Claude Opus 4.6
parent ad63a7b1af
commit 1973b24eaa
2 changed files with 16 additions and 12 deletions
@@ -88,10 +88,6 @@
</div>
</div>
</div>
<p class="text-xs font-semibold mt-2 truncate px-0.5"
:class="isDark ? 'text-white/90' : 'text-gray-900'">
{{ book.title }}
</p>
</button>
</div>
@@ -711,23 +711,26 @@ export function useContentPanel() {
const patterns: { re: RegExp; titleIdx: number; authorIdx: number }[] = [
// "Title" by Author
{ re: /["""]([^"""]{2,80})["""]\s+by\s+([A-Z][^,\n\.]{1,50}?)(?:\s*[,()\n\.]|$)/gi, titleIdx: 1, authorIdx: 2 },
// **Title** by Author
{ re: /\*\*([^*]{2,80})\*\*\s+by\s+([A-Z][^,\n\.]{1,50}?)(?:\s*[,()\n\.]|$)/g, titleIdx: 1, authorIdx: 2 },
// **Title** by/—/ Author (with optional italic on author)
{ re: /\*\*([^*]{2,80})\*\*\s+(?:by|—|)\s+\*?([A-Z][^*\n]{1,50}?)\*?(?:\s*[,()*\n]|$)/g, titleIdx: 1, authorIdx: 2 },
// - Title — Author or Title by Author (in list)
{ re: /(?:^|\n)\s*(?:\d+\.\s*|[-•]\s*)\*{0,2}([^*\n\-–—]{2,80}?)\*{0,2}\s+(?:by|—|)\s+([A-Z][^,\n]{1,50}?)(?:\s*[\n(,.]|$)/gm, titleIdx: 1, authorIdx: 2 },
{ re: /(?:^|\n)\s*(?:\d+\.\s*|[-•]\s*)\*{0,2}([^*\n\-–—]{2,80}?)\*{0,2}\s+(?:by|—|)\s+\*?([A-Z][^*\n]{1,50}?)\*?(?:\s*[,()*\n]|$)/gm, titleIdx: 1, authorIdx: 2 },
]
for (const { re, titleIdx, authorIdx } of patterns) {
let m: RegExpExecArray | null
const rx = new RegExp(re.source, re.flags)
while ((m = rx.exec(text)) !== null) {
const title = m[titleIdx].trim().replace(/^\*\*|\*\*$/g, '')
const title = m[titleIdx].trim().replace(/^\*\*|\*\*$/g, '').replace(/^\[|\]$/g, '')
const author = m[authorIdx].trim().replace(/^\*\*|\*\*$/g, '')
if (title.length < 2 || author.length < 2) continue
// Skip if it looks like a film/song/podcast tag
if (/\[\[(film|song|podcast|book)(_ext)?:/.test(title)) continue
// Skip numbers-only
if (/^\d{4}$/.test(title) || /^\d{4}$/.test(author)) continue
// Skip markdown links [text](url) — these are source references, not books
const afterMatch = text.substring(m.index + m[0].length, m.index + m[0].length + 200)
if (/^\s*\]\s*\(https?:/.test(afterMatch) || /\]\(https?:\/\//.test(m[0])) continue
const key = `${title.toLowerCase()}|${author.toLowerCase()}`
if (seen.has(key)) continue
seen.add(key)
@@ -763,11 +766,16 @@ export function useContentPanel() {
if (externalBooks.length > 0) return externalBooks
// Only do fallback pattern matching if the query or response looks book-related
if (!isBookQuery(userQuery) && !isBookLikeResponse(text)) return []
// Skip if other content types are already tagged
if (extractFilmIds(text).length > 0 || /\[\[film_ext:/.test(text)) return []
if (extractSongIds(text).length > 0 || /\[\[song_ext:/.test(text)) return []
// Skip if other content types are the primary content (not just supplementary refs)
// When user explicitly asked about books, film/song tags are just cross-references
if (!isBookQuery(userQuery)) {
if (extractFilmIds(text).length > 0 || /\[\[film_ext:/.test(text)) return []
if (extractSongIds(text).length > 0 || /\[\[song_ext:/.test(text)) return []
}
if (isNewsLikeResponse(text)) return []
return extractBooksFromPatterns(text)
// Strip sources/references section at the end to avoid matching markdown links as books
const cleanText = text.replace(/\n---\n\s*(?:Sources|References|Links):?\s*\n[\s\S]*$/i, '')
return extractBooksFromPatterns(cleanText)
}
function extractExternalTVSeries(text: string): TVSeries[] {