Skip to content

Commit 057249c

Browse files
abelpzcursoragent
andcommitted
fix(tc-study): Markdown skeleton + cache, viewer caches, remove scripture book title
- MarkdownRenderer: show skeleton while loading; cache by content (no re-render on tab switch) - TN/TWL/TQ/TW/TA: content caches so switching tabs doesn't re-fetch (notes, words links, questions, word entry, TA article) - ScriptureViewer: remove book title above content (nav bar is sufficient) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0565403 commit 057249c

File tree

8 files changed

+191
-103
lines changed

8 files changed

+191
-103
lines changed

apps/tc-study/src/components/resources/ScriptureViewer/hooks/useContent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
import type { ProcessedScripture, ProcessedVerse } from '@bt-synergy/usfm-processor'
66
import { useEffect, useMemo, useState } from 'react'
7-
import type { BookInfo } from '../../../../contexts/types-only'
87
import { useCatalogManager, useCurrentReference, useNavigation } from '../../../../contexts'
8+
import type { BookInfo } from '../../../../contexts/types-only'
99
import { defaultSectionsService } from '../../../../lib/services/default-sections'
1010
import { extractVerseCountsFromContent } from '../../../../lib/versification'
1111
import { attachAlignmentSemanticIds } from '../utils/attachAlignmentSemanticIds'

apps/tc-study/src/components/resources/ScriptureViewer/index.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
import { Book, Bug } from 'lucide-react'
1414
import { useCallback, useEffect, useRef, useState } from 'react'
1515
import { useEvents } from 'linked-panels'
16-
import { useAnchorResource } from '../../../contexts/AppContext'
1716
import { useCatalogManager, useCurrentReference } from '../../../contexts'
1817
import type { VerseNavigationSignal } from '../../../signals/studioSignals'
19-
import { getBookTitle } from '../../../utils/bookNames'
2018
import { ResourceViewerHeader } from '../common/ResourceViewerHeader'
2119
import {
2220
DebugPanel,
@@ -37,7 +35,6 @@ export function ScriptureViewer({
3735
}: ScriptureViewerProps) {
3836
const currentRef = useCurrentReference()
3937
const catalogManager = useCatalogManager()
40-
const anchorResource = useAnchorResource()
4138
const { setBook, setChapter, setVerse, setEndChapter, setEndVerse } = currentRef
4239

4340
// Debug panel visibility state
@@ -181,12 +178,6 @@ export function ScriptureViewer({
181178
}
182179
}}
183180
>
184-
{/* Book title - centered, bold, serif, italic, larger than chapter number */}
185-
{currentRef.book && (
186-
<h2 className="text-center font-bold font-serif italic text-3xl text-gray-900 py-4">
187-
{getBookTitle(anchorResource, currentRef.book)}
188-
</h2>
189-
)}
190181
{/* Content - scrolling handled by parent container */}
191182
<div className="flex-1">
192183
<ScriptureContent

apps/tc-study/src/components/resources/TranslationAcademyViewer.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Displays Translation Academy content (training articles for translators)
55
*/
66

7-
import { AlertCircle, ArrowLeft, BookOpen, ChevronDown, ChevronUp, FileText, GraduationCap, Loader, Search } from 'lucide-react'
7+
import { AlertCircle, ArrowLeft, ChevronDown, ChevronUp, FileText, GraduationCap, Loader, Search } from 'lucide-react'
88
import { useEffect, useMemo, useState } from 'react'
99
import { useCatalogManager, useLoaderRegistry } from '../../contexts/CatalogContext'
1010

@@ -16,6 +16,9 @@ interface TranslationAcademyArticle {
1616
relatedArticles?: string[]
1717
}
1818

19+
const TA_ARTICLE_CACHE_MAX = 80
20+
const taArticleCache = new Map<string, TranslationAcademyArticle>()
21+
1922
interface TranslationAcademyViewerExtendedProps {
2023
resourceKey: string
2124
metadata?: any
@@ -143,9 +146,17 @@ export function TranslationAcademyViewer({
143146
}
144147
}, [metadata, selectedArticleId, initialEntryId])
145148

146-
// Load article content when selection changes
149+
// Load article content when selection changes (cached by resourceKey+articleId so switching tabs doesn't re-fetch)
147150
useEffect(() => {
148-
if (!selectedArticleId || !resourceKey) {
151+
if (!selectedArticleId || !resourceKey) return
152+
153+
const key = `ta:${resourceKey}:${selectedArticleId}`
154+
const hit = taArticleCache.get(key)
155+
if (hit !== undefined) {
156+
setArticle(hit)
157+
setError(null)
158+
setLoading(false)
159+
setViewMode('article')
149160
return
150161
}
151162

@@ -160,6 +171,8 @@ export function TranslationAcademyViewer({
160171
}
161172

162173
const loadedArticle = await loader.loadContent(resourceKey, selectedArticleId)
174+
if (taArticleCache.size >= TA_ARTICLE_CACHE_MAX) taArticleCache.delete(taArticleCache.keys().next().value!)
175+
taArticleCache.set(key, loadedArticle)
163176
setArticle(loadedArticle)
164177
setViewMode('article')
165178
} catch (err: any) {

apps/tc-study/src/components/resources/TranslationNotesViewer/hooks/useTranslationNotesContent.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
11
/**
2-
* Hook for loading Translation Notes content
2+
* Hook for loading Translation Notes content.
3+
* Results are cached by resourceKey+book so switching tabs doesn't re-fetch.
34
*/
45

56
import type { TranslationNote } from '@bt-synergy/resource-parsers'
67
import { useEffect, useState } from 'react'
78
import { useLoaderRegistry } from '../../../../contexts/CatalogContext'
89

10+
const CACHE_MAX = 50
11+
const notesCache = new Map<string, { notes: TranslationNote[]; error: string | null }>()
12+
13+
function cacheKey(resourceKey: string, bookCode: string) {
14+
return `notes:${resourceKey}:${bookCode}`
15+
}
16+
917
export function useTranslationNotesContent(resourceKey: string, bookCode: string) {
1018
const loaderRegistry = useLoaderRegistry()
11-
const [notes, setNotes] = useState<TranslationNote[]>([])
12-
const [loading, setLoading] = useState(false)
13-
const [error, setError] = useState<string | null>(null)
19+
const cached = resourceKey && bookCode ? notesCache.get(cacheKey(resourceKey, bookCode)) : undefined
20+
const [notes, setNotes] = useState<TranslationNote[]>(cached?.notes ?? [])
21+
const [loading, setLoading] = useState(!cached)
22+
const [error, setError] = useState<string | null>(cached?.error ?? null)
1423

1524
useEffect(() => {
1625
if (!resourceKey || !bookCode) {
1726
setNotes([])
27+
setError(null)
28+
setLoading(false)
29+
return
30+
}
31+
32+
const key = cacheKey(resourceKey, bookCode)
33+
const hit = notesCache.get(key)
34+
if (hit !== undefined) {
35+
setNotes(hit.notes)
36+
setError(hit.error)
37+
setLoading(false)
1838
return
1939
}
2040

@@ -30,15 +50,16 @@ export function useTranslationNotesContent(resourceKey: string, bookCode: string
3050
throw new Error('Translation Notes loader not found')
3151
}
3252

33-
console.log(`📖 Loading translation notes for: ${resourceKey}/${bookCode}`)
3453
const processedNotes = await loader.loadContent(resourceKey, bookCode)
3554

3655
if (cancelled) return
3756

3857
if (processedNotes && processedNotes.notes) {
39-
setNotes(processedNotes.notes)
58+
const data = { notes: processedNotes.notes, error: null }
59+
if (notesCache.size >= CACHE_MAX) notesCache.delete(notesCache.keys().next().value!)
60+
notesCache.set(key, data)
61+
setNotes(data.notes)
4062
} else {
41-
console.warn('⚠️ No notes returned from loader')
4263
setNotes([])
4364
}
4465
} catch (err) {
@@ -50,25 +71,21 @@ export function useTranslationNotesContent(resourceKey: string, bookCode: string
5071
error: err instanceof Error ? err.message : String(err),
5172
})
5273

53-
// Check if it's a 404 (book not available)
54-
if (err instanceof Error && err.message.includes('404')) {
55-
setError(`Notes not available for ${bookCode.toUpperCase()}`)
56-
} else {
57-
setError(err instanceof Error ? err.message : 'Failed to load notes')
58-
}
74+
const errMsg = err instanceof Error && err.message.includes('404')
75+
? `Notes not available for ${bookCode.toUpperCase()}`
76+
: (err instanceof Error ? err.message : 'Failed to load notes')
77+
const data = { notes: [] as TranslationNote[], error: errMsg }
78+
if (notesCache.size >= CACHE_MAX) notesCache.delete(notesCache.keys().next().value!)
79+
notesCache.set(key, data)
80+
setError(errMsg)
5981
setNotes([])
6082
} finally {
61-
if (!cancelled) {
62-
setLoading(false)
63-
}
83+
if (!cancelled) setLoading(false)
6484
}
6585
}
6686

6787
loadNotes()
68-
69-
return () => {
70-
cancelled = true
71-
}
88+
return () => { cancelled = true }
7289
}, [resourceKey, bookCode, loaderRegistry])
7390

7491
return { notes, loading, error }

apps/tc-study/src/components/resources/TranslationQuestionsViewer/index.tsx

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* Translation Questions Viewer
3-
*
3+
*
44
* Displays comprehension questions and answers for Bible passages.
55
* Questions are filtered by the current verse range.
6+
* Results are cached by resourceKey+book so switching tabs doesn't re-fetch.
67
*/
78

89
import type { ProcessedQuestions } from '@bt-synergy/resource-parsers'
@@ -16,57 +17,66 @@ import { getBookTitle } from '../../../utils/bookNames'
1617
import { ResourceViewerHeader } from '../common/ResourceViewerHeader'
1718
import type { ResourceInfo } from '../../../contexts/types'
1819

20+
const TQ_CACHE_MAX = 50
21+
const questionsCache = new Map<string, ProcessedQuestions>()
22+
23+
function tqCacheKey(resourceKey: string, bookCode: string) {
24+
return `tq:${resourceKey}:${bookCode}`
25+
}
26+
1927
export function TranslationQuestionsViewer({ resourceKey, resource }: ResourceViewerProps & { resource: ResourceInfo }) {
20-
const [questions, setQuestions] = useState<ProcessedQuestions | null>(null)
21-
const [loading, setLoading] = useState(false)
22-
const [error, setError] = useState<string | null>(null)
23-
const [expandedQuestions, setExpandedQuestions] = useState<Set<string>>(new Set())
24-
2528
const loaderRegistry = useLoaderRegistry()
2629
const currentRef = useCurrentReference()
2730
const anchorResource = useAnchorResource()
28-
2931
const bookCode = currentRef.book || 'gen'
3032

33+
const cached = resourceKey && bookCode ? questionsCache.get(tqCacheKey(resourceKey, bookCode)) : undefined
34+
const [questions, setQuestions] = useState<ProcessedQuestions | null>(cached ?? null)
35+
const [loading, setLoading] = useState(!cached)
36+
const [error, setError] = useState<string | null>(null)
37+
const [expandedQuestions, setExpandedQuestions] = useState<Set<string>>(new Set())
38+
3139
// Load questions for current book
3240
useEffect(() => {
3341
if (!loaderRegistry || !resourceKey) return
3442

43+
const key = tqCacheKey(resourceKey, bookCode)
44+
const hit = questionsCache.get(key)
45+
if (hit !== undefined) {
46+
setQuestions(hit)
47+
setLoading(false)
48+
return
49+
}
50+
3551
let cancelled = false
3652

3753
const loadQuestions = async () => {
3854
setLoading(true)
3955
setError(null)
40-
56+
4157
try {
4258
const loader = loaderRegistry.getLoader('questions')
4359
if (!loader) {
4460
throw new Error('Translation Questions loader not found')
4561
}
4662

47-
console.log(`📖 Loading translation questions for: ${resourceKey}/${bookCode}`)
4863
const content = await loader.loadContent(resourceKey, bookCode)
49-
50-
if (cancelled) return
5164

65+
if (cancelled) return
66+
if (questionsCache.size >= TQ_CACHE_MAX) questionsCache.delete(questionsCache.keys().next().value!)
67+
questionsCache.set(key, content)
5268
setQuestions(content)
5369
} catch (err) {
5470
if (cancelled) return
55-
5671
console.error(`Failed to load questions for ${bookCode}:`, err)
5772
setError(err instanceof Error ? err.message : 'Failed to load questions')
5873
} finally {
59-
if (!cancelled) {
60-
setLoading(false)
61-
}
74+
if (!cancelled) setLoading(false)
6275
}
6376
}
6477

6578
loadQuestions()
66-
67-
return () => {
68-
cancelled = true
69-
}
79+
return () => { cancelled = true }
7080
}, [resourceKey, bookCode, loaderRegistry])
7181

7282
// Filter questions for current chapter/verse range

apps/tc-study/src/components/resources/TranslationWordsViewer.tsx

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ interface TranslationWord {
1717
seeAlso?: string[]
1818
}
1919

20+
const TW_WORD_CACHE_MAX = 80
21+
const twWordCache = new Map<string, TranslationWord>()
22+
2023
interface TranslationWordsViewerExtendedProps {
2124
resourceKey: string
2225
metadata?: any
@@ -159,52 +162,47 @@ export function TranslationWordsViewer({
159162
}
160163
}, [metadata, selectedWordId, initialEntryId])
161164

162-
// Load word content when selection changes
165+
// Load word content when selection changes (cached by resourceKey+entryId so switching tabs doesn't re-fetch)
163166
useEffect(() => {
164-
if (!selectedWordId || !resourceKey) {
165-
console.log('[TranslationWordsViewer] Skipping load - selectedWordId:', selectedWordId, 'resourceKey:', resourceKey)
166-
return
167-
}
167+
if (!selectedWordId || !resourceKey) return
168168

169-
// Skip loading if selectedWordId is a directory (e.g., "bible")
170169
if (selectedWordId === 'bible' || !selectedWordId.includes('/')) {
171-
console.warn('⚠️ Skipping load for directory entry:', selectedWordId)
172170
setError('Please select a word entry, not a category')
173171
setLoading(false)
174172
return
175173
}
176174

177-
// Don't load if loaderRegistry is not available
178175
if (!loaderRegistry) {
179-
console.error('⚠️ LoaderRegistry not available, cannot load word')
180176
setError('Resource loader not available. Please refresh the page.')
181177
setLoading(false)
182178
return
183179
}
184180

181+
const key = `tw:${resourceKey}:${selectedWordId}`
182+
const hit = twWordCache.get(key)
183+
if (hit !== undefined) {
184+
setWord(hit)
185+
setError(null)
186+
setLoading(false)
187+
return
188+
}
189+
185190
const loadWord = async () => {
186191
setLoading(true)
187192
setError(null)
188-
193+
189194
try {
190195
const loader = loaderRegistry.getLoader('words')
191196
if (!loader) {
192197
throw new Error('Translation Words loader not found')
193198
}
194-
195-
console.log('📖 Loading word:', { resourceKey, entryId: selectedWordId })
196-
if (!selectedWordId || !selectedWordId.includes('/')) {
197-
throw new Error(`Invalid entry ID format: "${selectedWordId}". Expected format: "bible/category/term"`)
198-
}
199199
const wordData = await loader.loadContent(resourceKey, selectedWordId)
200-
setWord(wordData as TranslationWord)
200+
const w = wordData as TranslationWord
201+
if (twWordCache.size >= TW_WORD_CACHE_MAX) twWordCache.delete(twWordCache.keys().next().value!)
202+
twWordCache.set(key, w)
203+
setWord(w)
201204
} catch (err) {
202-
console.error('❌ Failed to load word:', {
203-
resourceKey,
204-
entryId: selectedWordId,
205-
error: err instanceof Error ? err.message : String(err),
206-
stack: err instanceof Error ? err.stack : undefined
207-
})
205+
console.error('❌ Failed to load word:', { resourceKey, entryId: selectedWordId, error: err })
208206
setError(err instanceof Error ? err.message : 'Failed to load word')
209207
} finally {
210208
setLoading(false)

0 commit comments

Comments
 (0)