diff --git a/app/[locale]/bible/page.tsx b/app/[locale]/bible/page.tsx index 23be61a..6fcfa44 100644 --- a/app/[locale]/bible/page.tsx +++ b/app/[locale]/bible/page.tsx @@ -1,5 +1,10 @@ +import { Suspense } from 'react' import BibleReader from './reader' export default function BiblePage() { - return + return ( + Loading...}> + + + ) } \ No newline at end of file diff --git a/app/[locale]/search/page.tsx b/app/[locale]/search/page.tsx index c7cec86..ce25a6e 100644 --- a/app/[locale]/search/page.tsx +++ b/app/[locale]/search/page.tsx @@ -1,988 +1,10 @@ -'use client' - -import { useState, useEffect, useRef, useCallback } from 'react' -import { useTranslations, useLocale } from 'next-intl' -import { useAuth } from '@/hooks/use-auth' -import { useRouter, useSearchParams } from 'next/navigation' -import { - Box, - Container, - Typography, - TextField, - Button, - Paper, - Card, - CardContent, - Chip, - List, - ListItem, - ListItemButton, - ListItemText, - IconButton, - Tooltip, - FormControl, - InputLabel, - Select, - MenuItem, - Checkbox, - FormControlLabel, - Collapse, - Divider, - Alert, - Skeleton, - Badge, - ButtonGroup, - InputAdornment, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Fab, - useTheme, - useMediaQuery -} from '@mui/material' -import { - Search, - FilterList, - History, - Bookmark, - BookmarkBorder, - Share, - Launch, - ExpandMore, - ExpandLess, - Clear, - TuneRounded, - MenuBook, - Article, - ContentCopy, - KeyboardArrowUp, - AutoAwesome, - SearchOff -} from '@mui/icons-material' - -interface SearchResult { - id: string - verseId: string - book: string - bookId: string - bookKey?: string - chapter: number - verse: number - text: string - relevance: number - context?: { - before?: string - after?: string - } -} - -interface SearchFilters { - testament: 'all' | 'old' | 'new' - bookKeys: string[] - searchType: 'phrase' | 'words' | 'exact' - version: string - showContext: boolean - sortBy: 'relevance' | 'book' | 'reference' -} - -interface BookOption { - id: string - name: string - bookKey: string - orderNum: number - testament: string -} - -interface SearchSuggestion { - text: string - type: 'history' | 'popular' | 'autocomplete' - count?: number -} +import { Suspense } from 'react' +import SearchContent from './search-content' export default function SearchPage() { - const theme = useTheme() - const isMobile = useMediaQuery(theme.breakpoints.down('md')) - const t = useTranslations('pages.search') - const locale = useLocale() - const router = useRouter() - const searchParams = useSearchParams() - const { user } = useAuth() - - // Core search state - const [searchQuery, setSearchQuery] = useState('') - const [results, setResults] = useState([]) - const [loading, setLoading] = useState(false) - const [totalResults, setTotalResults] = useState(0) - const [currentPage, setCurrentPage] = useState(1) - const [shouldSearchFromUrl, setShouldSearchFromUrl] = useState(false) - - // UI state - const [filtersOpen, setFiltersOpen] = useState(false) - const [showSuggestions, setShowSuggestions] = useState(false) - const [suggestions, setSuggestions] = useState([]) - const [searchHistory, setSearchHistory] = useState([]) - const [savedSearches, setSavedSearches] = useState([]) - - // Search configuration - const [filters, setFilters] = useState({ - testament: 'all', - bookKeys: [], - searchType: 'words', - version: '', - showContext: true, - sortBy: 'relevance' - }) - - // Data - const [books, setBooks] = useState([]) - const [versions, setVersions] = useState>([]) - const [bookmarks, setBookmarks] = useState<{[key: string]: boolean}>({}) - - // Refs - const searchInputRef = useRef(null) - const resultsRef = useRef(null) - - const oldTestamentBooks = books.filter(b => b.orderNum <= 39) - const newTestamentBooks = books.filter(b => b.orderNum > 39) - - const popularSearches = [ - { text: t('popular.items.0'), type: 'popular' as const, count: 1250 }, - { text: t('popular.items.1'), type: 'popular' as const, count: 980 }, - { text: t('popular.items.2'), type: 'popular' as const, count: 875 }, - { text: t('popular.items.3'), type: 'popular' as const, count: 720 }, - { text: t('popular.items.4'), type: 'popular' as const, count: 680 }, - { text: t('popular.items.5'), type: 'popular' as const, count: 650 }, - { text: t('popular.items.6'), type: 'popular' as const, count: 580 }, - { text: t('popular.items.7'), type: 'popular' as const, count: 520 }, - { text: t('popular.items.8'), type: 'popular' as const, count: 480 }, - { text: t('popular.items.9'), type: 'popular' as const, count: 420 } - ] - - // Load search history and saved searches - useEffect(() => { - if (!user) return - - const userKey = `searchHistory_${user.id}` - const history = localStorage.getItem(userKey) - const saved = localStorage.getItem('savedSearches') - - if (history) { - setSearchHistory(JSON.parse(history)) - } - if (saved) { - setSavedSearches(JSON.parse(saved)) - } - }, [user]) - - // Load search parameters from URL (run only once on mount) - useEffect(() => { - const urlQuery = searchParams.get('q') - const urlTestament = searchParams.get('testament') - const urlSearchType = searchParams.get('type') - const urlBooks = searchParams.get('books') - - if (urlQuery) { - setSearchQuery(urlQuery) - setShouldSearchFromUrl(true) - } - - if (urlTestament || urlSearchType || urlBooks) { - setFilters(prev => ({ - ...prev, - ...(urlTestament && { testament: urlTestament as any }), - ...(urlSearchType && { searchType: urlSearchType as any }), - ...(urlBooks && { bookKeys: urlBooks.split(',').filter(Boolean) }) - })) - } - }, []) // Empty dependency array to run only once - - // Load versions and books - useEffect(() => { - const loadVersions = async () => { - try { - const response = await fetch(`/api/bible/versions?locale=${locale}`) - const data = await response.json() - const versionList: Array<{ id: string; name: string; abbreviation: string; isDefault: boolean }> = data.versions || [] - setVersions(versionList) - - const defaultVersion = versionList.find(v => v.isDefault) || versionList[0] - if (defaultVersion) { - setFilters(prev => ({ ...prev, version: defaultVersion.abbreviation })) - } - } catch (error) { - console.error('Failed to load versions:', error) - } - } - - loadVersions() - }, [locale]) - - useEffect(() => { - const loadBooks = async () => { - if (!filters.version) return - - try { - const response = await fetch(`/api/bible/books?locale=${locale}&version=${filters.version}`) - const data = await response.json() - const bookList = (data.books || []).map((b: any) => ({ - id: b.id, - name: b.name, - bookKey: b.bookKey, - orderNum: b.orderNum, - testament: b.testament - })) - setBooks(bookList) - } catch (error) { - console.error('Failed to load books:', error) - } - } - - loadBooks() - }, [locale, filters.version]) - - // Update suggestions based on search query - useEffect(() => { - if (!searchQuery.trim()) { - setSuggestions([]) - return - } - - const historySuggestions = searchHistory - .filter(h => h.toLowerCase().includes(searchQuery.toLowerCase())) - .slice(0, 3) - .map(text => ({ text, type: 'history' as const })) - - const popularSuggestions = popularSearches - .filter(p => p.text.toLowerCase().includes(searchQuery.toLowerCase())) - .slice(0, 3) - - setSuggestions([...historySuggestions, ...popularSuggestions]) - }, [searchQuery, searchHistory]) - - const handleSearch = useCallback(async (query?: string, page = 1) => { - const searchTerm = query || searchQuery - if (!searchTerm.trim()) return - - setLoading(true) - setCurrentPage(page) - - // Update search history (user-specific) - if (user) { - const userKey = `searchHistory_${user.id}` - const newHistory = [searchTerm, ...searchHistory.filter(s => s !== searchTerm)].slice(0, 20) - setSearchHistory(newHistory) - localStorage.setItem(userKey, JSON.stringify(newHistory)) - } - - try { - const params = new URLSearchParams({ - q: searchTerm, - page: page.toString(), - limit: '20', - testament: filters.testament, - searchType: filters.searchType, - showContext: filters.showContext.toString(), - sortBy: filters.sortBy, - locale, - version: filters.version, - bookKeys: filters.bookKeys.join(',') - }) - - const response = await fetch(`/api/search/verses?${params}`) - - if (!response.ok) { - throw new Error('Search failed') - } - - const data = await response.json() - setResults(data.results || []) - setTotalResults(data.total || 0) - - // Update URL with search parameters for sharing and tracking - const urlParams = new URLSearchParams({ - q: searchTerm, - ...(filters.testament !== 'all' && { testament: filters.testament }), - ...(filters.searchType !== 'words' && { type: filters.searchType }), - ...(filters.bookKeys.length > 0 && { books: filters.bookKeys.join(',') }) - }) - - const newUrl = `${window.location.pathname}?${urlParams.toString()}` - window.history.replaceState({}, '', newUrl) - - // Scroll to results on mobile - if (isMobile && resultsRef.current) { - resultsRef.current.scrollIntoView({ behavior: 'smooth' }) - } - - } catch (error) { - console.error('Search error:', error) - // Show demo results for development - setResults([ - { - id: '1', - verseId: 'verse-1', - book: 'Ioan', - bookId: 'john', - chapter: 3, - verse: 16, - text: 'Fiindcă atât de mult a iubit Dumnezeu lumea, că a dat pe singurul Său Fiu, pentru ca oricine crede în El să nu piară, ci să aibă viața veșnică.', - relevance: 0.95, - context: { - before: 'Și nimeni nu s-a suit în cer, afară de cel ce s-a coborât din cer, adică Fiul omului care este în cer.', - after: 'Căci Dumnezeu nu a trimis pe Fiul Său în lume ca să judece lumea, ci pentru ca lumea să fie mântuită prin El.' - } - }, - { - id: '2', - verseId: 'verse-2', - book: '1 Corinteni', - bookId: '1-corinthians', - chapter: 13, - verse: 4, - text: 'Dragostea este îndelung răbdătoare, dragostea este binevoitoare; dragostea nu pizmuiește, dragostea nu se laudă, nu se îngâmfă;', - relevance: 0.89, - context: { - before: 'De aș vorbi în limbile oamenilor și ale îngerilor, dar să n-am dragoste, am ajuns aramă sunătoare sau chimval zăngănitor.', - after: 'nu lucrează cu necuviință, nu caută ale sale, nu se mânie, nu ține seama de rău;' - } - } - ]) - setTotalResults(2) - } finally { - setLoading(false) - setShowSuggestions(false) - } - }, [searchQuery, filters, searchHistory, locale, isMobile]) - - // Trigger search from URL after handleSearch is defined - useEffect(() => { - if (shouldSearchFromUrl && searchQuery) { - handleSearch(searchQuery, 1) - setShouldSearchFromUrl(false) - } - }, [shouldSearchFromUrl, searchQuery, handleSearch]) - - const handleVerseBookmark = useCallback(async (result: SearchResult) => { - if (!user) return - - const token = localStorage.getItem('authToken') - if (!token) return - - try { - const isBookmarked = bookmarks[result.verseId] - - if (isBookmarked) { - const response = await fetch(`/api/bookmarks/verse?verseId=${result.verseId}&locale=${locale}`, { - method: 'DELETE', - headers: { 'Authorization': `Bearer ${token}` } - }) - - if (response.ok) { - setBookmarks(prev => { - const updated = { ...prev } - delete updated[result.verseId] - return updated - }) - } - } else { - const response = await fetch(`/api/bookmarks/verse?locale=${locale}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ verseId: result.verseId }) - }) - - if (response.ok) { - setBookmarks(prev => ({ ...prev, [result.verseId]: true })) - } - } - } catch (error) { - console.error('Bookmark error:', error) - } - }, [user, bookmarks, locale]) - - const handleCopyVerse = useCallback((result: SearchResult) => { - const text = `${result.book} ${result.chapter}:${result.verse} - ${result.text}` - navigator.clipboard.writeText(text) - }, []) - - const handleNavigateToVerse = useCallback((result: SearchResult) => { - const bookIdentifier = result.bookId || result.bookKey || result.book - router.push(`/${locale}/bible?book=${bookIdentifier}&chapter=${result.chapter}&verse=${result.verse}`) - }, [router, locale]) - - const clearFilters = useCallback(() => { - setFilters(prev => ({ - ...prev, - testament: 'all', - bookKeys: [], - searchType: 'words' - })) - }, []) - - const highlightSearchTerm = useCallback((text: string, query: string) => { - if (!query.trim()) return text - - const words = query.trim().split(/\s+/) - let highlightedText = text - - words.forEach(word => { - const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi') - highlightedText = highlightedText.replace(regex, '$1') - }) - - return - }, []) - - const activeFiltersCount = (filters.testament !== 'all' ? 1 : 0) + - filters.bookKeys.length + - (filters.searchType !== 'words' ? 1 : 0) - return ( - - - {/* Header */} - - - - {t('title')} - - - {t('subtitle')} - - - - {/* Search Bar */} - - - - { - setSearchQuery(e.target.value) - setShowSuggestions(true) - }} - onFocus={() => setShowSuggestions(true)} - onKeyPress={(e) => e.key === 'Enter' && handleSearch()} - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: searchQuery && ( - - { - setSearchQuery('') - setShowSuggestions(false) - }} - > - - - - ), - sx: { - fontSize: '1.1rem', - '& .MuiOutlinedInput-root': { - borderRadius: 2 - } - } - }} - /> - - {/* Search Suggestions */} - 0}> - - - {suggestions.map((suggestion, index) => ( - - { - setSearchQuery(suggestion.text) - handleSearch(suggestion.text) - }} - > - - {suggestion.type === 'history' && } - {suggestion.type === 'popular' && } - - {suggestion.text} - - {suggestion.count && ( - - )} - - } - /> - - - ))} - - - - - - handleSearch()} - disabled={!searchQuery.trim() || loading} - sx={{ - minWidth: 120, - height: 56, - borderRadius: 2, - textTransform: 'none', - fontSize: '1rem' - }} - > - {loading ? t('searching') : t('button.search')} - - - setFiltersOpen(!filtersOpen)} - startIcon={} - sx={{ - height: 56, - borderRadius: 2, - textTransform: 'none' - }} - > - - {t('filters.title')} - - - - - {/* Quick Filters */} - - - setFilters(prev => ({ ...prev, searchType: 'words' }))} - > - {t('searchTypes.anyWords')} - - setFilters(prev => ({ ...prev, searchType: 'phrase' }))} - > - {t('searchTypes.exactPhrase')} - - - - - setFilters(prev => ({ ...prev, testament: 'all' }))} - > - {t('filters.options.all')} - - setFilters(prev => ({ ...prev, testament: 'old' }))} - > - {t('filters.options.old')} - - setFilters(prev => ({ ...prev, testament: 'new' }))} - > - {t('filters.options.new')} - - - - {activeFiltersCount > 0 && ( - }> - {t('filters.clear')} - - )} - - - - {/* Advanced Filters */} - - - - {/* First row of filters */} - - - - {t('filters.version')} - setFilters(prev => ({ ...prev, version: e.target.value }))} - > - {versions.map(v => ( - - {v.name} - - ))} - - - - - - - Sort by - setFilters(prev => ({ ...prev, sortBy: e.target.value as any }))} - > - Relevance - Book order - Reference - - - - - - setFilters(prev => ({ ...prev, showContext: e.target.checked }))} - /> - } - label="Show context" - /> - - - - {/* Popular searches */} - - - {t('popular.title')} - - - {popularSearches.slice(0, 10).map((search, index) => ( - { - setSearchQuery(search.text) - handleSearch(search.text) - setFiltersOpen(false) - }} - /> - ))} - - - - {/* Book selection */} - - - {t('filters.specificBooks')} - - - {books.map(book => ( - { - const exists = filters.bookKeys.includes(book.bookKey) - const newBookKeys = exists - ? filters.bookKeys.filter(k => k !== book.bookKey) - : [...filters.bookKeys, book.bookKey] - setFilters(prev => ({ ...prev, bookKeys: newBookKeys })) - }} - color={filters.bookKeys.includes(book.bookKey) ? 'primary' : 'default'} - /> - ))} - - - - - - - - {/* Sidebar */} - - {/* Search History */} - {searchHistory.length > 0 && ( - - - - - {t('history.title')} - - - {searchHistory.slice(0, 8).map((query, index) => ( - { - setSearchQuery(query) - handleSearch(query) - }} - /> - ))} - - - - )} - - - - {/* Main Results */} - - - {/* Results Header */} - {(results.length > 0 || loading) && ( - - - {loading ? t('searching') : t('results', { count: totalResults })} - - {results.length > 0 && ( - - Page {currentPage} - - )} - - )} - - {/* Loading State */} - {loading && ( - - {Array.from({ length: 5 }).map((_, index) => ( - - - - - - - - - - - - ))} - - )} - - {/* Search Results */} - {results.length > 0 && !loading && ( - - {results.map((result) => ( - - - - handleNavigateToVerse(result)} - > - {result.book} {result.chapter}:{result.verse} - - - - - - {user && ( - - handleVerseBookmark(result)} - color={bookmarks[result.verseId] ? 'warning' : 'default'} - > - {bookmarks[result.verseId] ? : } - - - )} - - - handleCopyVerse(result)} - > - - - - - - handleNavigateToVerse(result)} - > - - - - - - - {filters.showContext && result.context?.before && ( - - ...{result.context.before} - - )} - - - {highlightSearchTerm(result.text, searchQuery)} - - - {filters.showContext && result.context?.after && ( - - {result.context.after}... - - )} - - - ))} - - {/* Load More / Pagination */} - {totalResults > results.length && ( - - handleSearch(searchQuery, currentPage + 1)} - disabled={loading} - > - Load more results - - - )} - - )} - - {/* No Results */} - {!loading && searchQuery && results.length === 0 && ( - - - - {t('noResults.title')} - - - {t('noResults.description')} - - - Try clearing filters - - - )} - - {/* Empty State */} - {!searchQuery && !loading && ( - - - - {t('empty.title')} - - - {t('empty.description')} - - - - {t('filters.title')} → {t('popular.title')} - - - )} - - - - - {/* Scroll to Top */} - {results.length > 5 && ( - window.scrollTo({ top: 0, behavior: 'smooth' })} - sx={{ - position: 'fixed', - bottom: 16, - right: 16 - }} - > - - - )} - - + Loading...}> + + ) -} +} \ No newline at end of file diff --git a/app/[locale]/search/search-content.tsx b/app/[locale]/search/search-content.tsx new file mode 100644 index 0000000..6a43129 --- /dev/null +++ b/app/[locale]/search/search-content.tsx @@ -0,0 +1,988 @@ +'use client' + +import { useState, useEffect, useRef, useCallback } from 'react' +import { useTranslations, useLocale } from 'next-intl' +import { useAuth } from '@/hooks/use-auth' +import { useRouter, useSearchParams } from 'next/navigation' +import { + Box, + Container, + Typography, + TextField, + Button, + Paper, + Card, + CardContent, + Chip, + List, + ListItem, + ListItemButton, + ListItemText, + IconButton, + Tooltip, + FormControl, + InputLabel, + Select, + MenuItem, + Checkbox, + FormControlLabel, + Collapse, + Divider, + Alert, + Skeleton, + Badge, + ButtonGroup, + InputAdornment, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Fab, + useTheme, + useMediaQuery +} from '@mui/material' +import { + Search, + FilterList, + History, + Bookmark, + BookmarkBorder, + Share, + Launch, + ExpandMore, + ExpandLess, + Clear, + TuneRounded, + MenuBook, + Article, + ContentCopy, + KeyboardArrowUp, + AutoAwesome, + SearchOff +} from '@mui/icons-material' + +interface SearchResult { + id: string + verseId: string + book: string + bookId: string + bookKey?: string + chapter: number + verse: number + text: string + relevance: number + context?: { + before?: string + after?: string + } +} + +interface SearchFilters { + testament: 'all' | 'old' | 'new' + bookKeys: string[] + searchType: 'phrase' | 'words' | 'exact' + version: string + showContext: boolean + sortBy: 'relevance' | 'book' | 'reference' +} + +interface BookOption { + id: string + name: string + bookKey: string + orderNum: number + testament: string +} + +interface SearchSuggestion { + text: string + type: 'history' | 'popular' | 'autocomplete' + count?: number +} + +export default function SearchContent() { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + const t = useTranslations('pages.search') + const locale = useLocale() + const router = useRouter() + const searchParams = useSearchParams() + const { user } = useAuth() + + // Core search state + const [searchQuery, setSearchQuery] = useState('') + const [results, setResults] = useState([]) + const [loading, setLoading] = useState(false) + const [totalResults, setTotalResults] = useState(0) + const [currentPage, setCurrentPage] = useState(1) + const [shouldSearchFromUrl, setShouldSearchFromUrl] = useState(false) + + // UI state + const [filtersOpen, setFiltersOpen] = useState(false) + const [showSuggestions, setShowSuggestions] = useState(false) + const [suggestions, setSuggestions] = useState([]) + const [searchHistory, setSearchHistory] = useState([]) + const [savedSearches, setSavedSearches] = useState([]) + + // Search configuration + const [filters, setFilters] = useState({ + testament: 'all', + bookKeys: [], + searchType: 'words', + version: '', + showContext: true, + sortBy: 'relevance' + }) + + // Data + const [books, setBooks] = useState([]) + const [versions, setVersions] = useState>([]) + const [bookmarks, setBookmarks] = useState<{[key: string]: boolean}>({}) + + // Refs + const searchInputRef = useRef(null) + const resultsRef = useRef(null) + + const oldTestamentBooks = books.filter(b => b.orderNum <= 39) + const newTestamentBooks = books.filter(b => b.orderNum > 39) + + const popularSearches = [ + { text: t('popular.items.0'), type: 'popular' as const, count: 1250 }, + { text: t('popular.items.1'), type: 'popular' as const, count: 980 }, + { text: t('popular.items.2'), type: 'popular' as const, count: 875 }, + { text: t('popular.items.3'), type: 'popular' as const, count: 720 }, + { text: t('popular.items.4'), type: 'popular' as const, count: 680 }, + { text: t('popular.items.5'), type: 'popular' as const, count: 650 }, + { text: t('popular.items.6'), type: 'popular' as const, count: 580 }, + { text: t('popular.items.7'), type: 'popular' as const, count: 520 }, + { text: t('popular.items.8'), type: 'popular' as const, count: 480 }, + { text: t('popular.items.9'), type: 'popular' as const, count: 420 } + ] + + // Load search history and saved searches + useEffect(() => { + if (!user) return + + const userKey = `searchHistory_${user.id}` + const history = localStorage.getItem(userKey) + const saved = localStorage.getItem('savedSearches') + + if (history) { + setSearchHistory(JSON.parse(history)) + } + if (saved) { + setSavedSearches(JSON.parse(saved)) + } + }, [user]) + + // Load search parameters from URL (run only once on mount) + useEffect(() => { + const urlQuery = searchParams.get('q') + const urlTestament = searchParams.get('testament') + const urlSearchType = searchParams.get('type') + const urlBooks = searchParams.get('books') + + if (urlQuery) { + setSearchQuery(urlQuery) + setShouldSearchFromUrl(true) + } + + if (urlTestament || urlSearchType || urlBooks) { + setFilters(prev => ({ + ...prev, + ...(urlTestament && { testament: urlTestament as any }), + ...(urlSearchType && { searchType: urlSearchType as any }), + ...(urlBooks && { bookKeys: urlBooks.split(',').filter(Boolean) }) + })) + } + }, []) // Empty dependency array to run only once + + // Load versions and books + useEffect(() => { + const loadVersions = async () => { + try { + const response = await fetch(`/api/bible/versions?locale=${locale}`) + const data = await response.json() + const versionList: Array<{ id: string; name: string; abbreviation: string; isDefault: boolean }> = data.versions || [] + setVersions(versionList) + + const defaultVersion = versionList.find(v => v.isDefault) || versionList[0] + if (defaultVersion) { + setFilters(prev => ({ ...prev, version: defaultVersion.abbreviation })) + } + } catch (error) { + console.error('Failed to load versions:', error) + } + } + + loadVersions() + }, [locale]) + + useEffect(() => { + const loadBooks = async () => { + if (!filters.version) return + + try { + const response = await fetch(`/api/bible/books?locale=${locale}&version=${filters.version}`) + const data = await response.json() + const bookList = (data.books || []).map((b: any) => ({ + id: b.id, + name: b.name, + bookKey: b.bookKey, + orderNum: b.orderNum, + testament: b.testament + })) + setBooks(bookList) + } catch (error) { + console.error('Failed to load books:', error) + } + } + + loadBooks() + }, [locale, filters.version]) + + // Update suggestions based on search query + useEffect(() => { + if (!searchQuery.trim()) { + setSuggestions([]) + return + } + + const historySuggestions = searchHistory + .filter(h => h.toLowerCase().includes(searchQuery.toLowerCase())) + .slice(0, 3) + .map(text => ({ text, type: 'history' as const })) + + const popularSuggestions = popularSearches + .filter(p => p.text.toLowerCase().includes(searchQuery.toLowerCase())) + .slice(0, 3) + + setSuggestions([...historySuggestions, ...popularSuggestions]) + }, [searchQuery, searchHistory]) + + const handleSearch = useCallback(async (query?: string, page = 1) => { + const searchTerm = query || searchQuery + if (!searchTerm.trim()) return + + setLoading(true) + setCurrentPage(page) + + // Update search history (user-specific) + if (user) { + const userKey = `searchHistory_${user.id}` + const newHistory = [searchTerm, ...searchHistory.filter(s => s !== searchTerm)].slice(0, 20) + setSearchHistory(newHistory) + localStorage.setItem(userKey, JSON.stringify(newHistory)) + } + + try { + const params = new URLSearchParams({ + q: searchTerm, + page: page.toString(), + limit: '20', + testament: filters.testament, + searchType: filters.searchType, + showContext: filters.showContext.toString(), + sortBy: filters.sortBy, + locale, + version: filters.version, + bookKeys: filters.bookKeys.join(',') + }) + + const response = await fetch(`/api/search/verses?${params}`) + + if (!response.ok) { + throw new Error('Search failed') + } + + const data = await response.json() + setResults(data.results || []) + setTotalResults(data.total || 0) + + // Update URL with search parameters for sharing and tracking + const urlParams = new URLSearchParams({ + q: searchTerm, + ...(filters.testament !== 'all' && { testament: filters.testament }), + ...(filters.searchType !== 'words' && { type: filters.searchType }), + ...(filters.bookKeys.length > 0 && { books: filters.bookKeys.join(',') }) + }) + + const newUrl = `${window.location.pathname}?${urlParams.toString()}` + window.history.replaceState({}, '', newUrl) + + // Scroll to results on mobile + if (isMobile && resultsRef.current) { + resultsRef.current.scrollIntoView({ behavior: 'smooth' }) + } + + } catch (error) { + console.error('Search error:', error) + // Show demo results for development + setResults([ + { + id: '1', + verseId: 'verse-1', + book: 'Ioan', + bookId: 'john', + chapter: 3, + verse: 16, + text: 'Fiindcă atât de mult a iubit Dumnezeu lumea, că a dat pe singurul Său Fiu, pentru ca oricine crede în El să nu piară, ci să aibă viața veșnică.', + relevance: 0.95, + context: { + before: 'Și nimeni nu s-a suit în cer, afară de cel ce s-a coborât din cer, adică Fiul omului care este în cer.', + after: 'Căci Dumnezeu nu a trimis pe Fiul Său în lume ca să judece lumea, ci pentru ca lumea să fie mântuită prin El.' + } + }, + { + id: '2', + verseId: 'verse-2', + book: '1 Corinteni', + bookId: '1-corinthians', + chapter: 13, + verse: 4, + text: 'Dragostea este îndelung răbdătoare, dragostea este binevoitoare; dragostea nu pizmuiește, dragostea nu se laudă, nu se îngâmfă;', + relevance: 0.89, + context: { + before: 'De aș vorbi în limbile oamenilor și ale îngerilor, dar să n-am dragoste, am ajuns aramă sunătoare sau chimval zăngănitor.', + after: 'nu lucrează cu necuviință, nu caută ale sale, nu se mânie, nu ține seama de rău;' + } + } + ]) + setTotalResults(2) + } finally { + setLoading(false) + setShowSuggestions(false) + } + }, [searchQuery, filters, searchHistory, locale, isMobile]) + + // Trigger search from URL after handleSearch is defined + useEffect(() => { + if (shouldSearchFromUrl && searchQuery) { + handleSearch(searchQuery, 1) + setShouldSearchFromUrl(false) + } + }, [shouldSearchFromUrl, searchQuery, handleSearch]) + + const handleVerseBookmark = useCallback(async (result: SearchResult) => { + if (!user) return + + const token = localStorage.getItem('authToken') + if (!token) return + + try { + const isBookmarked = bookmarks[result.verseId] + + if (isBookmarked) { + const response = await fetch(`/api/bookmarks/verse?verseId=${result.verseId}&locale=${locale}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (response.ok) { + setBookmarks(prev => { + const updated = { ...prev } + delete updated[result.verseId] + return updated + }) + } + } else { + const response = await fetch(`/api/bookmarks/verse?locale=${locale}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ verseId: result.verseId }) + }) + + if (response.ok) { + setBookmarks(prev => ({ ...prev, [result.verseId]: true })) + } + } + } catch (error) { + console.error('Bookmark error:', error) + } + }, [user, bookmarks, locale]) + + const handleCopyVerse = useCallback((result: SearchResult) => { + const text = `${result.book} ${result.chapter}:${result.verse} - ${result.text}` + navigator.clipboard.writeText(text) + }, []) + + const handleNavigateToVerse = useCallback((result: SearchResult) => { + const bookIdentifier = result.bookId || result.bookKey || result.book + router.push(`/${locale}/bible?book=${bookIdentifier}&chapter=${result.chapter}&verse=${result.verse}`) + }, [router, locale]) + + const clearFilters = useCallback(() => { + setFilters(prev => ({ + ...prev, + testament: 'all', + bookKeys: [], + searchType: 'words' + })) + }, []) + + const highlightSearchTerm = useCallback((text: string, query: string) => { + if (!query.trim()) return text + + const words = query.trim().split(/\s+/) + let highlightedText = text + + words.forEach(word => { + const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi') + highlightedText = highlightedText.replace(regex, '$1') + }) + + return + }, []) + + const activeFiltersCount = (filters.testament !== 'all' ? 1 : 0) + + filters.bookKeys.length + + (filters.searchType !== 'words' ? 1 : 0) + + return ( + + + {/* Header */} + + + + {t('title')} + + + {t('subtitle')} + + + + {/* Search Bar */} + + + + { + setSearchQuery(e.target.value) + setShowSuggestions(true) + }} + onFocus={() => setShowSuggestions(true)} + onKeyPress={(e) => e.key === 'Enter' && handleSearch()} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + { + setSearchQuery('') + setShowSuggestions(false) + }} + > + + + + ), + sx: { + fontSize: '1.1rem', + '& .MuiOutlinedInput-root': { + borderRadius: 2 + } + } + }} + /> + + {/* Search Suggestions */} + 0}> + + + {suggestions.map((suggestion, index) => ( + + { + setSearchQuery(suggestion.text) + handleSearch(suggestion.text) + }} + > + + {suggestion.type === 'history' && } + {suggestion.type === 'popular' && } + + {suggestion.text} + + {suggestion.count && ( + + )} + + } + /> + + + ))} + + + + + + handleSearch()} + disabled={!searchQuery.trim() || loading} + sx={{ + minWidth: 120, + height: 56, + borderRadius: 2, + textTransform: 'none', + fontSize: '1rem' + }} + > + {loading ? t('searching') : t('button.search')} + + + setFiltersOpen(!filtersOpen)} + startIcon={} + sx={{ + height: 56, + borderRadius: 2, + textTransform: 'none' + }} + > + + {t('filters.title')} + + + + + {/* Quick Filters */} + + + setFilters(prev => ({ ...prev, searchType: 'words' }))} + > + {t('searchTypes.anyWords')} + + setFilters(prev => ({ ...prev, searchType: 'phrase' }))} + > + {t('searchTypes.exactPhrase')} + + + + + setFilters(prev => ({ ...prev, testament: 'all' }))} + > + {t('filters.options.all')} + + setFilters(prev => ({ ...prev, testament: 'old' }))} + > + {t('filters.options.old')} + + setFilters(prev => ({ ...prev, testament: 'new' }))} + > + {t('filters.options.new')} + + + + {activeFiltersCount > 0 && ( + }> + {t('filters.clear')} + + )} + + + + {/* Advanced Filters */} + + + + {/* First row of filters */} + + + + {t('filters.version')} + setFilters(prev => ({ ...prev, version: e.target.value }))} + > + {versions.map(v => ( + + {v.name} + + ))} + + + + + + + Sort by + setFilters(prev => ({ ...prev, sortBy: e.target.value as any }))} + > + Relevance + Book order + Reference + + + + + + setFilters(prev => ({ ...prev, showContext: e.target.checked }))} + /> + } + label="Show context" + /> + + + + {/* Popular searches */} + + + {t('popular.title')} + + + {popularSearches.slice(0, 10).map((search, index) => ( + { + setSearchQuery(search.text) + handleSearch(search.text) + setFiltersOpen(false) + }} + /> + ))} + + + + {/* Book selection */} + + + {t('filters.specificBooks')} + + + {books.map(book => ( + { + const exists = filters.bookKeys.includes(book.bookKey) + const newBookKeys = exists + ? filters.bookKeys.filter(k => k !== book.bookKey) + : [...filters.bookKeys, book.bookKey] + setFilters(prev => ({ ...prev, bookKeys: newBookKeys })) + }} + color={filters.bookKeys.includes(book.bookKey) ? 'primary' : 'default'} + /> + ))} + + + + + + + + {/* Sidebar */} + + {/* Search History */} + {searchHistory.length > 0 && ( + + + + + {t('history.title')} + + + {searchHistory.slice(0, 8).map((query, index) => ( + { + setSearchQuery(query) + handleSearch(query) + }} + /> + ))} + + + + )} + + + + {/* Main Results */} + + + {/* Results Header */} + {(results.length > 0 || loading) && ( + + + {loading ? t('searching') : t('results', { count: totalResults })} + + {results.length > 0 && ( + + Page {currentPage} + + )} + + )} + + {/* Loading State */} + {loading && ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + + + + + + + + + ))} + + )} + + {/* Search Results */} + {results.length > 0 && !loading && ( + + {results.map((result) => ( + + + + handleNavigateToVerse(result)} + > + {result.book} {result.chapter}:{result.verse} + + + + + + {user && ( + + handleVerseBookmark(result)} + color={bookmarks[result.verseId] ? 'warning' : 'default'} + > + {bookmarks[result.verseId] ? : } + + + )} + + + handleCopyVerse(result)} + > + + + + + + handleNavigateToVerse(result)} + > + + + + + + + {filters.showContext && result.context?.before && ( + + ...{result.context.before} + + )} + + + {highlightSearchTerm(result.text, searchQuery)} + + + {filters.showContext && result.context?.after && ( + + {result.context.after}... + + )} + + + ))} + + {/* Load More / Pagination */} + {totalResults > results.length && ( + + handleSearch(searchQuery, currentPage + 1)} + disabled={loading} + > + Load more results + + + )} + + )} + + {/* No Results */} + {!loading && searchQuery && results.length === 0 && ( + + + + {t('noResults.title')} + + + {t('noResults.description')} + + + Try clearing filters + + + )} + + {/* Empty State */} + {!searchQuery && !loading && ( + + + + {t('empty.title')} + + + {t('empty.description')} + + + + {t('filters.title')} → {t('popular.title')} + + + )} + + + + + {/* Scroll to Top */} + {results.length > 5 && ( + window.scrollTo({ top: 0, behavior: 'smooth' })} + sx={{ + position: 'fixed', + bottom: 16, + right: 16 + }} + > + + + )} + + + ) +} \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 42a4e33..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,69 +0,0 @@ -version: '3.8' - -services: - postgres: - image: pgvector/pgvector:pg16 - restart: always - environment: - POSTGRES_DB: bible_chat - POSTGRES_USER: bible_admin - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - bible_network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U bible_admin -d bible_chat"] - interval: 30s - timeout: 10s - retries: 3 - - app: - build: - context: . - dockerfile: docker/Dockerfile.prod - restart: always - environment: - DATABASE_URL: postgresql://bible_admin:${DB_PASSWORD}@postgres:5432/bible_chat - AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY} - AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} - AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT} - OLLAMA_API_URL: ${OLLAMA_API_URL} - JWT_SECRET: ${JWT_SECRET} - NEXTAUTH_URL: ${NEXTAUTH_URL} - NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} - NODE_ENV: production - depends_on: - postgres: - condition: service_healthy - networks: - - bible_network - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] - interval: 30s - timeout: 10s - retries: 3 - - nginx: - image: nginx:alpine - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf - - ./certbot/conf:/etc/letsencrypt - - ./certbot/www:/var/www/certbot - depends_on: - app: - condition: service_healthy - networks: - - bible_network - -networks: - bible_network: - driver: bridge - -volumes: - postgres_data: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 52c9d48..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: '3.8' -services: - postgres: - image: pgvector/pgvector:pg16 - environment: - POSTGRES_DB: bible_chat - POSTGRES_USER: bible_admin - POSTGRES_PASSWORD: ${DB_PASSWORD:-password} - volumes: - - postgres_data:/var/lib/postgresql/data - - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql - ports: - - "5432:5432" - networks: - - bible_network - - app: - build: - context: . - dockerfile: docker/Dockerfile.dev - volumes: - - .:/app - - /app/node_modules - ports: - - "3000:3000" - - "3001:3001" # WebSocket port - depends_on: - - postgres - environment: - DATABASE_URL: postgresql://bible_admin:${DB_PASSWORD:-password}@postgres:5432/bible_chat - AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY} - AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} - AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT} - OLLAMA_API_URL: ${OLLAMA_API_URL} - JWT_SECRET: ${JWT_SECRET:-default-jwt-secret-change-in-production} - NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000} - NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-default-nextauth-secret-change-in-production} - networks: - - bible_network - -networks: - bible_network: - driver: bridge - -volumes: - postgres_data: \ No newline at end of file diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev deleted file mode 100644 index a93727d..0000000 --- a/docker/Dockerfile.dev +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -# Install dependencies -COPY package*.json ./ -RUN npm ci - -# Copy source code -COPY . . - -# Expose ports -EXPOSE 3000 3001 - -# Start development server -CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod deleted file mode 100644 index 0e6e709..0000000 --- a/docker/Dockerfile.prod +++ /dev/null @@ -1,45 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -# Install dependencies -COPY package*.json ./ -RUN npm ci --only=production && npm cache clean --force - -# Copy source code -COPY . . - -# Generate Prisma client -RUN npx prisma generate - -# Build the application -RUN npm run build - -FROM node:20-alpine AS runner - -WORKDIR /app - -ENV NODE_ENV production - -# Create non-root user -RUN addgroup -g 1001 -S nodejs -RUN adduser -S nextjs -u 1001 - -# Copy built application -COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma - -# Copy package.json for npm scripts -COPY --from=builder /app/package.json ./package.json - -USER nextjs - -EXPOSE 3000 - -ENV PORT 3000 -ENV HOSTNAME "0.0.0.0" - -CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf deleted file mode 100644 index 645276f..0000000 --- a/docker/nginx/nginx.conf +++ /dev/null @@ -1,51 +0,0 @@ -upstream app { - server app:3000; -} - -server { - listen 80; - server_name _; - - client_max_body_size 10M; - - # Security headers - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options DENY; - add_header X-XSS-Protection "1; mode=block"; - - location / { - proxy_pass http://app; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } - - # Static files caching - location /_next/static { - proxy_pass http://app; - add_header Cache-Control "public, max-age=31536000, immutable"; - } - - # Favicon - location /favicon.ico { - proxy_pass http://app; - add_header Cache-Control "public, max-age=86400"; - } -} \ No newline at end of file diff --git a/temp/admin-dashboard-mvp.md b/temp/admin-dashboard-mvp.md deleted file mode 100644 index c99c468..0000000 --- a/temp/admin-dashboard-mvp.md +++ /dev/null @@ -1,347 +0,0 @@ -# BiblicalGuide Admin Dashboard - MVP Features - -## 1. Dashboard Overview (Home) - -### Key Metrics Cards -- **Total Users** (number + % change from last week) -- **Daily Active Users** (DAU - real-time count) -- **AI Conversations Today** (count + cost estimate) -- **Active Now** (users currently online) - -### Quick Stats -- **New signups** (last 24 hours) -- **Total bookmarks created** (all-time) -- **Prayer requests** (last 24 hours) -- **Server status** (API health, response time) - -### Activity Feed (Live) -- New user registrations -- Prayer wall posts -- High-engagement AI conversations -- Error alerts -- System notifications - -## 2. User Management - -### User List View -``` -Search: [_________] Filter: [All Users ▼] - -| Email | Name | Joined | Last Active | AI Chats | Status | Actions | -|-------|------|--------|-------------|----------|--------|---------| -| user@example.com | John D. | 2024-01-15 | 2 hrs ago | 45 | Active | [View] [Ban] | -``` - -### User Actions -- **View Profile** (see detailed user activity) -- **Send Email** (direct communication) -- **Reset Password** (force password reset) -- **Suspend/Ban** (temporary or permanent) -- **Delete Account** (GDPR compliance) - -### User Details Modal -- Registration date and method -- Total AI conversations -- Bookmarks count -- Last 10 chat topics -- Prayer requests posted -- Account status history - -## 3. AI Chat Monitoring - -### Conversations Overview -- **Total conversations** (today/week/month) -- **Average response time** (target: <5 seconds) -- **Error rate** (failed responses %) -- **Cost tracking** (OpenAI API spend) - -### Live Chat Monitor -``` -Recent Conversations (Auto-refresh every 30s) - -User: "What does the Bible say about forgiveness?" -AI: "The Bible speaks extensively about forgiveness..." -[View Full] [Flag] [Quality: ⭐⭐⭐⭐⭐] - -User: "Help me understand Romans 8" -AI: "Romans 8 is one of the most powerful chapters..." -[View Full] [Flag] [Quality: ⭐⭐⭐⭐⭐] -``` - -### Quality Control -- **Flag inappropriate requests** -- **Review AI responses** for theological accuracy -- **Common questions** (FAQ generation) -- **Failed responses** log -- **Response ratings** (if users rate them) - -### Cost Management -- Daily API usage ($X.XX) -- Projection for current month -- Per-user average cost -- High-usage user alerts - -## 4. Content Moderation - -### Prayer Wall Moderation -``` -Pending Review (3) - -"Please pray for my family..." -[Approve] [Edit] [Reject] [Ban User] - -"Spam content here..." -[Approve] [Edit] [Reject] [Ban User] -``` - -### Moderation Queue -- **Prayer requests** (pending approval) -- **Reported content** (user flags) -- **AI conversation flags** (inappropriate) -- **Bulk actions** (approve/reject all) - -### Auto-Moderation Settings -- Profanity filter (on/off) -- Spam detection threshold -- Auto-approve trusted users -- Keyword blacklist management - -## 5. Analytics Dashboard - -### User Analytics -- **Growth Chart** (daily new users - line graph) -- **Retention Funnel** - - Day 1: 100% - - Day 7: 45% - - Day 30: 30% -- **User Segments** - - Power users (>10 chats/week) - - Regular (3-10 chats/week) - - Casual (<3 chats/week) - - Dormant (no activity 7+ days) - -### Engagement Metrics -- **Most Read Bible Books** (top 10 bar chart) -- **Popular Chat Topics** (word cloud) -- **Peak Usage Hours** (heatmap) -- **Feature Usage** (bookmarks vs chat vs prayer wall) - -### Simple Conversion Tracking -- Sign-up to first chat -- First chat to bookmark -- Single to returning user -- Free to premium (when implemented) - -## 6. System Administration - -### Quick Actions -- **Broadcast Message** (banner to all users) -- **Maintenance Mode** (on/off toggle) -- **Clear Cache** (Redis flush) -- **Backup Database** (manual trigger) - -### API Management -- **OpenAI API Status** - - Current balance - - Rate limit status - - Error rate - - Switch API keys -- **Bible API Status** - - Response time - - Cache hit rate - - Error logs - -### Error Monitoring -``` -Recent Errors (Last 24 hours) - -Type | Count | Last Occurred | Status ------|-------|---------------|-------- -API Timeout | 12 | 10 min ago | [View] [Resolve] -Login Failed | 45 | 2 min ago | [View] [Ignore] -DB Connection | 0 | - | OK -``` - -### Server Health -- CPU usage (%) -- Memory usage (%) -- Database connections -- API response times -- Disk space remaining - -## 7. Communications - -### Email Templates (Basic) -- Welcome email -- Password reset -- Daily verse (if enabled) -- Account suspended -- System announcements - -### Bulk Email -- **Send to**: All users / Active last 7 days / Segment -- **Subject**: [_________] -- **Message**: [Rich text editor] -- **Schedule**: Now / Later -- [Send Test] [Schedule] [Send Now] - -### In-App Notifications -- Create announcement banner -- Target specific user segments -- Set expiration time -- Track dismissal rate - -## 8. Settings & Configuration - -### App Settings -- **Site name**: BiblicalGuide -- **Support email**: support@biblical-guide.com -- **Daily verse**: Enabled/Disabled -- **Prayer wall**: Public/Moderated/Disabled -- **AI model**: GPT-4/GPT-3.5 -- **Rate limits**: X chats per user per day - -### Feature Toggles -- [ ] AI Chat enabled -- [ ] Prayer Wall enabled -- [ ] User registration open -- [ ] Daily verse emails -- [ ] Social sharing -- [ ] Maintenance mode - -### Admin Users -``` -Admin Users (3) - -Email | Role | Last Login | Actions -------|------|------------|-------- -admin@site.com | Super Admin | 1 hr ago | [Edit] [Remove] -support@site.com | Moderator | 2 days ago | [Edit] [Remove] -[+ Add Admin] -``` - -## 9. Simple Reporting - -### Daily Report Email -- New users count -- Total AI conversations -- API costs -- Error summary -- Top chat topics -- System health status - -### Export Data -- **User list** (CSV) -- **Chat logs** (last 30 days) -- **Prayer requests** (CSV) -- **Analytics summary** (PDF) - -## 10. Security Section - -### Security Log -``` -Recent Security Events - -Time | User | Event | IP Address | Action ------|------|-------|------------|-------- -10:23 AM | john@... | Failed login (3x) | 192.168.1.1 | [Block IP] -09:45 AM | Admin login | Success | 10.0.0.1 | - -``` - -### Security Actions -- View failed login attempts -- IP blocking/allowlisting -- Force logout all users -- Require password reset (bulk) -- 2FA enforcement settings - -## Implementation Priority - -### Phase 1 - Critical (Week 1) -1. **Login/Authentication** for admins -2. **Dashboard overview** (basic metrics) -3. **User list** with basic actions -4. **AI conversation monitoring** (view only) - -### Phase 2 - Essential (Week 2) -1. **Prayer wall moderation** -2. **User management** (suspend/ban) -3. **Basic analytics** (users, engagement) -4. **Error monitoring** - -### Phase 3 - Important (Week 3) -1. **Email communications** -2. **System settings** -3. **Export functionality** -4. **Cost tracking** - -## Tech Stack for Admin - -### Frontend -- **React** with **Recharts** for graphs -- **Tailwind CSS** with **shadcn/ui** components -- **React Table** for data tables -- **React Query** for data fetching - -### Backend Additions -- **Admin authentication** (separate from users) -- **Role-based access** (admin, moderator) -- **Audit logging** (track all admin actions) -- **Scheduled jobs** (daily reports) - -### Database Schema Additions -```sql --- Admin users table -admin_users ( - id, email, password_hash, role, - last_login, created_at -) - --- Audit log table -audit_logs ( - id, admin_id, action, target_user_id, - details, ip_address, timestamp -) - --- System settings table -settings ( - key, value, updated_at, updated_by -) -``` - -## Access Control Levels - -### Super Admin -- Full access to everything -- Can manage other admins -- System configuration -- Database operations - -### Moderator -- Content moderation -- User management (not deletion) -- View analytics -- Cannot change system settings - -### Support -- View user details -- Reset passwords -- View chat logs -- Cannot ban/delete users - -## Security Considerations - -1. **Separate admin authentication** (not regular user accounts) -2. **IP allowlisting** for admin access -3. **Audit log** all admin actions -4. **Session timeout** after 30 min inactive -5. **2FA required** for all admin accounts -6. **Read-only mode** for most views (explicit edit mode) - -## Success Metrics for Admin - -- **Response time** to user issues < 1 hour -- **Moderation queue** cleared daily -- **System uptime** > 99.9% -- **API costs** within budget -- **User complaints** resolved < 24 hours \ No newline at end of file diff --git a/temp/ai-chat-improvements-plan.md b/temp/ai-chat-improvements-plan.md deleted file mode 100644 index 7c5d277..0000000 --- a/temp/ai-chat-improvements-plan.md +++ /dev/null @@ -1,364 +0,0 @@ -# AI Chat Improvements Plan - -## Overview -Enhance the AI chat system with persistent chat history and conversation memory to provide a more engaging and contextual user experience. - -## Current State Analysis -- ✅ Basic AI chat with Azure OpenAI integration -- ✅ ReactMarkdown rendering for formatted responses -- ✅ Floating chat component with fullscreen mode -- ✅ Language-specific responses (Romanian/English) -- ❌ No chat persistence between sessions -- ❌ Limited conversation memory (only last 3 messages) -- ❌ No user-specific chat history -- ❌ No chat management UI - -## Goals -1. **Persistent Chat History**: Store chat conversations in database per user and language -2. **Enhanced Memory**: Maintain longer conversation context for better AI responses -3. **Chat Management**: Allow users to view, organize, and manage their chat history -4. **Multi-language Support**: Separate chat histories for Romanian and English - -## Technical Implementation Plan - -### 1. Database Schema Extensions - -#### 1.1 Chat Conversations Table -```prisma -model ChatConversation { - id String @id @default(cuid()) - userId String? // Optional for anonymous users - title String // Auto-generated from first message - language String // 'ro' or 'en' - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastMessageAt DateTime @default(now()) - - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - messages ChatMessage[] - - @@index([userId, language, lastMessageAt]) - @@index([isActive, lastMessageAt]) -} - -model ChatMessage { - id String @id @default(cuid()) - conversationId String - role ChatMessageRole - content String @db.Text - timestamp DateTime @default(now()) - metadata Json? // For storing additional context - - conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) - - @@index([conversationId, timestamp]) -} - -enum ChatMessageRole { - USER - ASSISTANT - SYSTEM -} -``` - -#### 1.2 Update User Model -```prisma -model User { - // ... existing fields - chatConversations ChatConversation[] -} -``` - -### 2. API Endpoints - -#### 2.1 Chat Conversations API (`/api/chat/conversations`) -- **GET**: List user's conversations (paginated, filtered by language) -- **POST**: Create new conversation -- **DELETE /:id**: Delete conversation - -#### 2.2 Enhanced Chat API (`/api/chat`) -- **Modify existing POST**: Include conversation management -- **GET /:conversationId**: Get conversation history -- **PUT /:conversationId**: Update conversation (rename, etc.) - -#### 2.3 Chat Messages API (`/api/chat/:conversationId/messages`) -- **GET**: Get all messages in conversation (paginated) -- **POST**: Add message to conversation - -### 3. Frontend Components - -#### 3.1 Enhanced Floating Chat Component -```typescript -interface FloatingChatProps { - conversationId?: string - initialMessage?: string -} - -// New state management -const [conversations, setConversations] = useState([]) -const [activeConversationId, setActiveConversationId] = useState(null) -const [messages, setMessages] = useState([]) -const [isHistoryOpen, setIsHistoryOpen] = useState(false) -``` - -#### 3.2 Chat History Sidebar -```typescript -interface ChatHistorySidebarProps { - conversations: Conversation[] - activeConversationId: string | null - onSelectConversation: (id: string) => void - onNewConversation: () => void - onDeleteConversation: (id: string) => void - language: string -} -``` - -#### 3.3 Conversation List Item -```typescript -interface ConversationListItemProps { - conversation: Conversation - isActive: boolean - onClick: () => void - onDelete: () => void - onRename: (newTitle: string) => void -} -``` - -### 4. Implementation Phases - -#### Phase 1: Database Schema & Basic API -1. **Create Prisma migrations** for new tables -2. **Implement conversation CRUD APIs** -3. **Add database seed scripts** for testing -4. **Update existing chat API** to work with conversations - -#### Phase 2: Enhanced Memory System -1. **Modify chat API** to include more conversation context -2. **Implement intelligent context selection** (last 10-15 messages) -3. **Add conversation summarization** for very long chats -4. **Optimize token usage** with smart context management - -#### Phase 3: Frontend Chat Management -1. **Add conversation state management** to floating chat -2. **Implement chat history sidebar** -3. **Add conversation creation/deletion** functionality -4. **Implement conversation switching** within chat - -#### Phase 4: Advanced Features -1. **Auto-generate conversation titles** from first message -2. **Add conversation search/filtering** -3. **Implement conversation sharing** (optional) -4. **Add conversation export** functionality - -### 5. Detailed Implementation Steps - -#### 5.1 Database Setup -```bash -# Create migration -npx prisma migrate dev --name add-chat-conversations - -# Update database -npx prisma db push - -# Generate client -npx prisma generate -``` - -#### 5.2 API Implementation Strategy - -**Chat API Enhancement (`/api/chat/route.ts`)**: -```typescript -// Modified request schema -const chatRequestSchema = z.object({ - message: z.string().min(1), - conversationId: z.string().optional(), - locale: z.string().default('ro'), - userId: z.string().optional() -}) - -// Enhanced response -interface ChatResponse { - success: boolean - response: string - conversationId: string - messageId: string -} -``` - -**Conversation Management Logic**: -```typescript -async function handleChatMessage( - message: string, - conversationId?: string, - locale: string = 'ro', - userId?: string -) { - // 1. Get or create conversation - const conversation = conversationId - ? await getConversation(conversationId) - : await createConversation(userId, locale, message) - - // 2. Get conversation history (last 15 messages) - const history = await getConversationHistory(conversation.id, 15) - - // 3. Generate AI response with full context - const aiResponse = await generateBiblicalResponse(message, locale, history) - - // 4. Save both user message and AI response - await saveMessages(conversation.id, [ - { role: 'user', content: message }, - { role: 'assistant', content: aiResponse } - ]) - - // 5. Update conversation metadata - await updateConversationActivity(conversation.id) - - return { response: aiResponse, conversationId: conversation.id } -} -``` - -#### 5.3 Frontend State Management - -**Enhanced Floating Chat Hook**: -```typescript -function useFloatingChat() { - const [conversations, setConversations] = useState([]) - const [activeConversation, setActiveConversation] = useState(null) - const [messages, setMessages] = useState([]) - const [isLoading, setIsLoading] = useState(false) - - const loadConversations = useCallback(async () => { - // Fetch user's conversations - }, []) - - const switchConversation = useCallback(async (conversationId: string) => { - // Load conversation messages - }, []) - - const startNewConversation = useCallback(() => { - // Reset state for new conversation - }, []) - - const sendMessage = useCallback(async (message: string) => { - // Send message with conversation context - }, [activeConversation]) - - return { - conversations, - activeConversation, - messages, - isLoading, - loadConversations, - switchConversation, - startNewConversation, - sendMessage - } -} -``` - -### 6. UI/UX Enhancements - -#### 6.1 Chat History Sidebar Layout -``` -┌─────────────────┬──────────────────────┐ -│ Chat History │ Active Chat │ -│ │ │ -│ ○ New Chat │ ┌─ Messages ─────┐ │ -│ │ │ │ │ -│ ╔═ Today ═══ │ │ User: Question │ │ -│ ║ ○ Bible Q&A │ │ AI: Response │ │ -│ ║ ○ Prayer Help │ │ ... │ │ -│ ╚═══════════ │ │ │ │ -│ │ └────────────────┘ │ -│ ╔═ Yesterday ═ │ ┌─ Input ──────────┐ │ -│ ║ ○ Verse Study │ │ [Type message... ]│ │ -│ ╚═══════════ │ └──────────────────┘ │ -└─────────────────┴──────────────────────┘ -``` - -#### 6.2 Mobile-Responsive Design -- **Mobile**: Stack history as overlay/drawer -- **Tablet**: Side-by-side with collapsed history -- **Desktop**: Full side-by-side layout - -### 7. Performance Considerations - -#### 7.1 Database Optimization -- **Indexes**: conversation lookups, message pagination -- **Pagination**: Limit conversation and message queries -- **Cleanup**: Archive old conversations after 6 months - -#### 7.2 Frontend Optimization -- **Lazy loading**: Load conversations on demand -- **Virtualization**: For large message lists -- **Caching**: Cache conversation metadata - -#### 7.3 AI Context Management -- **Smart truncation**: Summarize old messages if context too long -- **Relevance scoring**: Prioritize recent and relevant messages -- **Token optimization**: Balance context richness vs cost - -### 8. Security & Privacy - -#### 8.1 Data Protection -- **User isolation**: Strict user-based access control -- **Encryption**: Sensitive conversation content -- **Retention policy**: Auto-delete after configurable period - -#### 8.2 Anonymous Users -- **Session-based storage**: For non-authenticated users -- **Migration path**: Convert anonymous chats to user accounts - -### 9. Testing Strategy - -#### 9.1 Unit Tests -- Conversation CRUD operations -- Message history management -- AI context generation - -#### 9.2 Integration Tests -- Full chat flow with persistence -- Conversation switching -- Multi-language handling - -#### 9.3 User Testing -- Chat history navigation -- Conversation management -- Mobile responsiveness - -### 10. Deployment Considerations - -#### 10.1 Migration Strategy -- **Backward compatibility**: Existing chat continues to work -- **Data migration**: Convert existing session data if applicable -- **Feature flags**: Gradual rollout of new features - -#### 10.2 Monitoring -- **Conversation metrics**: Creation, length, engagement -- **Performance monitoring**: API response times -- **Error tracking**: Failed chat operations - -## Success Metrics - -1. **User Engagement**: Longer chat sessions, return conversations -2. **Conversation Quality**: Better AI responses with context -3. **User Satisfaction**: Positive feedback on chat experience -4. **Technical Performance**: Fast conversation loading, reliable persistence - -## Timeline Estimate - -- **Phase 1** (Database & API): 3-4 days -- **Phase 2** (Enhanced Memory): 2-3 days -- **Phase 3** (Frontend Management): 4-5 days -- **Phase 4** (Advanced Features): 3-4 days - -**Total**: ~2-3 weeks for complete implementation - -## Next Steps - -1. Review and approve this plan -2. Begin with Phase 1: Database schema and basic API -3. Implement incremental improvements -4. Test thoroughly at each phase -5. Gather user feedback and iterate \ No newline at end of file diff --git a/temp/azure-embed3-bible-pgvector-guide.md b/temp/azure-embed3-bible-pgvector-guide.md deleted file mode 100644 index 6cce4d5..0000000 --- a/temp/azure-embed3-bible-pgvector-guide.md +++ /dev/null @@ -1,372 +0,0 @@ -# Azure OpenAI **embed-3** → Postgres + pgvector Ingestion Guide (Bible Corpus) - -**Goal**: Create a production‑ready Python script that ingests the full Bible (Markdown source) into **Postgres** with **pgvector** and **full‑text** metadata, using **Azure OpenAI `embed-3`** embeddings. The vectors will power a consumer chat assistant (Q&A & conversations about the Bible) and a backend agent that generates custom prayers. - -> Sample corpus used here: Romanian *Biblia Fidela* (Markdown). Structure contains books, chapters, verses (e.g., *Geneza 1:1…*) and a TOC in the file. fileciteturn0file0 - ---- - -## 0) Architecture at a glance - -- **Input**: Bible in Markdown (`*.md`) → parser → normalized records: *(book, chapter, verse, text, lang=ro)* -- **Embedding**: Azure OpenAI **embed-3** (prefer `text-embedding-3-large`, 3072‑D). Batch inputs to cut cost/latency. -- **Storage**: Postgres with: - - `pgvector` column `embedding vector(3072)` - - `tsvector` column for hybrid lexical search (Romanian or English config as needed) - - metadata columns for fast filtering (book, chapter, verse, testament, translation, language) -- **Indexes**: `ivfflat` over `embedding`, GIN over `tsv` (and btree over metadata) -- **Retrieval**: - - Dense vector kNN - - Hybrid: combine kNN score + BM25/tsvector - - Windowed context stitching (neighbor verses) for chat -- **Consumers**: - - Chat assistant: answer + cite (book:chapter:verse). - - Prayer agent: prompt‑compose with retrieved passages & user intents. - ---- - -## 1) Prerequisites - -### Postgres + pgvector -```bash -# Install pgvector (on Ubuntu) -sudo apt-get update && sudo apt-get install -y postgresql postgresql-contrib -# In psql as superuser: -CREATE EXTENSION IF NOT EXISTS vector; -``` - -### Python deps -```bash -python -m venv .venv && source .venv/bin/activate -pip install psycopg[binary] pgvector pydantic python-dotenv httpx tqdm rapidfuzz -``` - -> `httpx` for HTTP (async‑capable), `pgvector` adapter, `rapidfuzz` for optional de‑dup or heuristic joins, `tqdm` for progress. - -### Azure OpenAI -- Create **Embeddings** deployment for **`text-embedding-3-large`** (or `-small` if cost sensitive). Name it (e.g.) `embeddings`. -- Collect: - - `AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/` - - `AZURE_OPENAI_API_KEY=...` - - `AZURE_OPENAI_API_VERSION=2024-05-01-preview` *(or your current stable)* - - `AZURE_OPENAI_EMBED_DEPLOYMENT=embeddings` *(your deployment name)* - -Create `.env`: -```env -DATABASE_URL=postgresql://user:pass@localhost:5432/bible -AZURE_OPENAI_ENDPOINT=https://YOUR_RESOURCE.openai.azure.com/ -AZURE_OPENAI_API_KEY=YOUR_KEY -AZURE_OPENAI_API_VERSION=2024-05-01-preview -AZURE_OPENAI_EMBED_DEPLOYMENT=embeddings -EMBED_DIMS=3072 -BIBLE_MD_PATH=./Biblia-Fidela-limba-romana.md -LANG_CODE=ro -TRANSLATION_CODE=FIDELA -``` - ---- - -## 2) Database schema - -```sql --- One-time setup in your database -CREATE EXTENSION IF NOT EXISTS vector; - -CREATE TABLE IF NOT EXISTS bible_passages ( - id BIGSERIAL PRIMARY KEY, - testament TEXT NOT NULL, -- 'OT' or 'NT' - book TEXT NOT NULL, - chapter INT NOT NULL, - verse INT NOT NULL, - ref TEXT GENERATED ALWAYS AS (book || ' ' || chapter || ':' || verse) STORED, - lang TEXT NOT NULL DEFAULT 'ro', - translation TEXT NOT NULL DEFAULT 'FIDELA', - text_raw TEXT NOT NULL, -- exact verse text - text_norm TEXT NOT NULL, -- normalized/cleaned text (embedding input) - tsv tsvector, - embedding vector(3072), -- 1536 if using embed-3-small - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() -); - --- Uniqueness by canonical reference within translation/language -CREATE UNIQUE INDEX IF NOT EXISTS ux_ref_lang ON bible_passages (translation, lang, book, chapter, verse); - --- Full-text index (choose config; Romanian available if installed via ISPELL; else use 'simple' or 'english') --- If you have pg_catalog.romanian, use that. Else fallback to 'simple' but keep lexemes. -CREATE INDEX IF NOT EXISTS idx_tsv ON bible_passages USING GIN (tsv); - --- Vector index (choose nlist to match data size; we set after populating table) --- First create a flat index for small data, or IVFFLAT for scale: --- Requires ANALYZE beforehand and SET enable_seqscan=off for kNN plans. -``` - -After loading, build the IVFFLAT index (the table must be populated first): -```sql --- Example: around 31k verses ⇒ nlist ~ 100–200 is reasonable; tune per EXPLAIN ANALYZE -CREATE INDEX IF NOT EXISTS idx_vec_ivfflat -ON bible_passages USING ivfflat (embedding vector_cosine_ops) -WITH (lists = 200); -``` - -Trigger to keep `updated_at` fresh: -```sql -CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$ -BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; - -DROP TRIGGER IF EXISTS trg_bible_updated ON bible_passages; -CREATE TRIGGER trg_bible_updated BEFORE UPDATE ON bible_passages -FOR EACH ROW EXECUTE PROCEDURE touch_updated_at(); -``` - ---- - -## 3) Parsing & Chunking strategy (large, high‑quality) - -**Why verse‑level?** It’s the canonical granular unit for Bible QA. -**Context‑stitching**: during retrieval, fetch neighbor verses (±N) to maintain narrative continuity. -**Normalization** steps (for `text_norm`): -- Strip verse numbers and sidenotes if present in raw lines. -- Collapse whitespace, unify quotes, remove page headers/footers and TOC artifacts. -- Preserve punctuation; avoid stemming before embeddings. -- Lowercasing optional (OpenAI embeddings are case-robust). - -**Testament/book detection**: From headings and TOC present in the Markdown; detect Book → Chapter → Verse boundaries via regex. -Example regex heuristics (tune to your file): -- Book headers: `^(?P[A-ZĂÂÎȘȚ].+?)\s*$` (bounded by known canon order) -- Chapter headers: `^Capitolul\s+(?P\d+)` or `^CApitoLuL\s+(?P\d+)` (case variations) -- Verse lines: `^(?P\d+)\s+(.+)$` - -> The provided Markdown clearly shows book order (e.g., *Geneza*, *Exodul*, …; NT: *Matei*, *Marcu*, …) and verse lines like “**1** LA început…”. fileciteturn0file0 - ---- - -## 4) Python ingestion script - -> **Save as** `ingest_bible_pgvector.py` - -```python -import os, re, json, math, time, asyncio -from typing import List, Dict, Tuple, Iterable -from dataclasses import dataclass -from pathlib import Path -from dotenv import load_dotenv -import httpx -import psycopg -from psycopg.rows import dict_row - -load_dotenv() - -AZ_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT", "").rstrip("/") -AZ_API_KEY = os.getenv("AZURE_OPENAI_API_KEY") -AZ_API_VER = os.getenv("AZURE_OPENAI_API_VERSION", "2024-05-01-preview") -AZ_DEPLOYMENT = os.getenv("AZURE_OPENAI_EMBED_DEPLOYMENT", "embeddings") -EMBED_DIMS = int(os.getenv("EMBED_DIMS", "3072")) -DB_URL = os.getenv("DATABASE_URL") -BIBLE_MD_PATH = os.getenv("BIBLE_MD_PATH") -LANG_CODE = os.getenv("LANG_CODE", "ro") -TRANSLATION = os.getenv("TRANSLATION_CODE", "FIDELA") - -assert AZ_ENDPOINT and AZ_API_KEY and DB_URL and BIBLE_MD_PATH, "Missing required env vars" - -EMBED_URL = f"{AZ_ENDPOINT}/openai/deployments/{AZ_DEPLOYMENT}/embeddings?api-version={AZ_API_VER}" - -BOOKS_OT = [ - "Geneza","Exodul","Leviticul","Numeri","Deuteronom","Iosua","Judecători","Rut", - "1 Samuel","2 Samuel","1 Imparati","2 Imparati","1 Cronici","2 Cronici","Ezra","Neemia","Estera", - "Iov","Psalmii","Proverbe","Eclesiastul","Cântarea Cântărilor","Isaia","Ieremia","Plângerile", - "Ezechiel","Daniel","Osea","Ioel","Amos","Obadia","Iona","Mica","Naum","Habacuc","Țefania","Hagai","Zaharia","Maleahi" -] -BOOKS_NT = [ - "Matei","Marcu","Luca","Ioan","Faptele Apostolilor","Romani","1 Corinteni","2 Corinteni", - "Galateni","Efeseni","Filipeni","Coloseni","1 Tesaloniceni","2 Tesaloniceni","1 Timotei","2 Timotei", - "Titus","Filimon","Evrei","Iacov","1 Petru","2 Petru","1 Ioan","2 Ioan","3 Ioan","Iuda","Revelaţia" -] - -BOOK_CANON = {b:("OT" if b in BOOKS_OT else "NT") for b in BOOKS_OT + BOOKS_NT} - -@dataclass -class Verse: - testament: str - book: str - chapter: int - verse: int - text_raw: str - text_norm: str - -def normalize_text(s: str) -> str: - s = re.sub(r"\s+", " ", s.strip()) - s = s.replace(" ", " ") - return s - -BOOK_RE = re.compile(r"^(?P[A-ZĂÂÎȘȚ][^\n]+?)\s*$") -CH_RE = re.compile(r"^(?i:Capitolul|CApitoLuL)\s+(?P\d+)\b") -VERSE_RE = re.compile(r"^(?P\d+)\s+(?P.+)$") - -def parse_bible_md(md_text: str): - cur_book, cur_ch = None, None - testament = None - for line in md_text.splitlines(): - line = line.rstrip() - - # Book detection - m_book = BOOK_RE.match(line) - if m_book: - bname = m_book.group("book").strip() - if bname in BOOK_CANON: - cur_book = bname - testament = BOOK_CANON[bname] - cur_ch = None - continue - - m_ch = CH_RE.match(line) - if m_ch and cur_book: - cur_ch = int(m_ch.group("ch")) - continue - - m_v = VERSE_RE.match(line) - if m_v and cur_book and cur_ch: - vnum = int(m_v.group("v")) - body = m_v.group("body").strip() - raw = body - norm = normalize_text(body) - yield { - "testament": testament, "book": cur_book, "chapter": cur_ch, "verse": vnum, - "text_raw": raw, "text_norm": norm - } - -async def embed_batch(client, inputs): - payload = {"input": inputs} - headers = {"api-key": AZ_API_KEY, "Content-Type": "application/json"} - for attempt in range(6): - try: - r = await client.post(EMBED_URL, headers=headers, json=payload, timeout=60) - if r.status_code == 200: - data = r.json() - ordered = sorted(data["data"], key=lambda x: x["index"]) - return [d["embedding"] for d in ordered] - elif r.status_code in (429, 500, 503): - backoff = 2 ** attempt + (0.1 * attempt) - await asyncio.sleep(backoff) - else: - raise RuntimeError(f"Embedding error {r.status_code}: {r.text}") - except Exception: - backoff = 2 ** attempt + (0.1 * attempt) - await asyncio.sleep(backoff) - raise RuntimeError("Failed to embed after retries") - -UPSERT_SQL = """ -INSERT INTO bible_passages (testament, book, chapter, verse, lang, translation, text_raw, text_norm, tsv, embedding) -VALUES (%(testament)s, %(book)s, %(chapter)s, %(verse)s, %(lang)s, %(translation)s, %(text_raw)s, %(text_norm)s, - to_tsvector(COALESCE(%(ts_lang)s,'simple')::regconfig, %(text_norm)s), %(embedding)s) -ON CONFLICT (translation, lang, book, chapter, verse) DO UPDATE -SET text_raw=EXCLUDED.text_raw, - text_norm=EXCLUDED.text_norm, - tsv=EXCLUDED.tsv, - embedding=EXCLUDED.embedding, - updated_at=now(); -""" - -async def main(): - md_text = Path(BIBLE_MD_PATH).read_text(encoding="utf-8", errors="ignore") - verses = list(parse_bible_md(md_text)) - print(f"Parsed verses: {len(verses)}") - - batch_size = 128 - async with httpx.AsyncClient() as client, psycopg.connect(DB_URL, autocommit=False) as conn: - with conn.cursor() as cur: - for i in range(0, len(verses), batch_size): - batch = verses[i:i+batch_size] - inputs = [v["text_norm"] for v in batch] - embs = await embed_batch(client, inputs) - rows = [] - for v, e in zip(batch, embs): - rows.append({ - **v, - "lang": os.getenv("LANG_CODE","ro"), - "translation": os.getenv("TRANSLATION_CODE","FIDELA"), - "ts_lang": "romanian", - "embedding": e - }) - cur.executemany(UPSERT_SQL, rows) - conn.commit() - print(f"Upserted {len(rows)} … {i+len(rows)}/{len(verses)}") - print("Done. Build IVFFLAT index after ANALYZE.") - -if __name__ == "__main__": - import asyncio - asyncio.run(main()) -``` - -**Notes** -- If `romanian` text search config is unavailable, set `ts_lang='simple'`. -- For `embed-3-small`, set `EMBED_DIMS=1536` and change column type to `vector(1536)`. - ---- - -## 5) Post‑ingestion steps - -```sql -VACUUM ANALYZE bible_passages; -CREATE INDEX IF NOT EXISTS idx_vec_ivfflat -ON bible_passages USING ivfflat (embedding vector_cosine_ops) -WITH (lists = 200); -CREATE INDEX IF NOT EXISTS idx_book_ch ON bible_passages (book, chapter); -``` - ---- - -## 6) Retrieval patterns - -### A) Pure vector kNN (cosine) -```sql -SELECT ref, book, chapter, verse, text_raw, - 1 - (embedding <=> $1) AS cosine_sim -FROM bible_passages -ORDER BY embedding <=> $1 -LIMIT $2; -``` - -### B) Hybrid lexical + vector (weighted) -```sql -WITH v AS ( - SELECT id, 1 - (embedding <=> $1) AS vsim - FROM bible_passages - ORDER BY embedding <=> $1 - LIMIT 100 -), -l AS ( - SELECT id, ts_rank(tsv, $2) AS lrank - FROM bible_passages - WHERE tsv @@ $2 -) -SELECT bp.ref, bp.book, bp.chapter, bp.verse, bp.text_raw, - COALESCE(v.vsim, 0) * 0.7 + COALESCE(l.lrank, 0) * 0.3 AS score -FROM bible_passages bp -LEFT JOIN v ON v.id = bp.id -LEFT JOIN l ON l.id = bp.id -ORDER BY score DESC -LIMIT 20; -``` - ---- - -## 7) Chat & Prayer agent tips - -- **Answer grounding**: always cite `ref` (e.g., *Ioan 3:16*). -- **Multilingual output**: keep quotes in Romanian; explain in the user’s language. -- **Prayer agent**: constrain tone & doctrine; inject retrieved verses as anchors. - ---- - -## 8) Ops - -- Idempotent `UPSERT`. -- Backoff on 429/5xx. -- Consider keeping both `embed-3-large` and `-small` columns when migrating. - ---- - -## 9) License & attribution - -This guide references the structure of *Biblia Fidela* Markdown for ingestion demonstration. fileciteturn0file0 diff --git a/temp/bible-chat-implementation-plan.md b/temp/bible-chat-implementation-plan.md deleted file mode 100644 index 1725ffa..0000000 --- a/temp/bible-chat-implementation-plan.md +++ /dev/null @@ -1,1482 +0,0 @@ -# Biblical Guide Web App - Granular Implementation Plan - -Primary UI Language: Romanian -Secondary UI Language: English - -## Project Overview -A self-hosted Bible study web application with AI chat capabilities, real-time features, and comprehensive user management using PostgreSQL with extensions for all functionality. - -## Technology Stack - -### Core Technologies -- **Frontend**: Next.js 14 (App Router) + TypeScript -- **Backend**: Next.js API Routes + WebSocket Server -- **Database**: PostgreSQL 16 with extensions (pgvector, pg_cron, pg_jwt) -- **Cache**: PostgreSQL with UNLOGGED tables for caching -- **Real-time**: Socket.io for WebSocket connections -- **AI/LLM**: Azure OpenAI API + Ollama API -- **Containerization**: Docker + Docker Compose -- **Testing**: Jest + React Testing Library -- **Styling**: Tailwind CSS + shadcn/ui components - -## Project Structure -``` - -├── docker/ -│ ├── Dockerfile.dev -│ ├── Dockerfile.prod -│ └── nginx/ -│ └── nginx.conf -├── docker-compose.yml -├── docker-compose.prod.yml -├── src/ -│ ├── app/ -│ │ ├── (auth)/ -│ │ ├── (main)/ -│ │ ├── api/ -│ │ └── layout.tsx -│ ├── components/ -│ ├── lib/ -│ ├── hooks/ -│ ├── types/ -│ ├── utils/ -│ └── middleware.ts -├── prisma/ -│ ├── schema.prisma -│ ├── migrations/ -│ └── seed/ -├── scripts/ -│ ├── import-bible.ts -│ └── setup-db.sh -├── tests/ -├── public/ -└── config/ -``` - -## Phase 1: Foundation Setup (Days 1-3) - -### Step 1.1: Initialize Project and Docker Environment -```bash -# Create project directory -mkdir bible-chat-app && cd bible-chat-app - -# Initialize Next.js with TypeScript -npx create-next-app@latest . --typescript --tailwind --app --no-src-dir - -# Create Docker configuration -``` - -**Docker Compose Configuration (docker-compose.yml):** -```yaml -version: '3.8' -services: - postgres: - image: pgvector/pgvector:pg16 - environment: - POSTGRES_DB: bible_chat - POSTGRES_USER: bible_admin - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql - ports: - - "5432:5432" - - app: - build: - context: . - dockerfile: docker/Dockerfile.dev - volumes: - - .:/app - - /app/node_modules - ports: - - "3000:3000" - - "3001:3001" # WebSocket port - depends_on: - - postgres - environment: - DATABASE_URL: postgresql://bible_admin:${DB_PASSWORD}@postgres:5432/bible_chat - AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY} - OLLAMA_API_URL: ${OLLAMA_API_URL} -``` - -### Step 1.2: Database Schema Setup - -**Install Dependencies:** -```bash -npm install @prisma/client prisma @types/node -npm install -D @types/bcryptjs bcryptjs -``` - -**Prisma Schema (prisma/schema.prisma):** -```prisma -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id @default(uuid()) - email String @unique - passwordHash String - name String? - theme String @default("light") - fontSize String @default("medium") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastLoginAt DateTime? - - sessions Session[] - bookmarks Bookmark[] - notes Note[] - chatMessages ChatMessage[] - prayerRequests PrayerRequest[] - readingHistory ReadingHistory[] - preferences UserPreference[] -} - -model Session { - id String @id @default(uuid()) - userId String - token String @unique - expiresAt DateTime - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - - @@index([userId]) - @@index([token]) -} - -model BibleBook { - id Int @id - name String - testament String - orderNum Int - chapters BibleChapter[] - - @@index([testament]) -} - -model BibleChapter { - id String @id @default(uuid()) - bookId Int - chapterNum Int - verses BibleVerse[] - - book BibleBook @relation(fields: [bookId], references: [id]) - - @@unique([bookId, chapterNum]) - @@index([bookId]) -} - -model BibleVerse { - id String @id @default(uuid()) - chapterId String - verseNum Int - text String @db.Text - version String @default("KJV") - - chapter BibleChapter @relation(fields: [chapterId], references: [id]) - bookmarks Bookmark[] - notes Note[] - - @@unique([chapterId, verseNum, version]) - @@index([chapterId]) - @@index([version]) -} - -model ChatMessage { - id String @id @default(uuid()) - userId String - role String // 'user' or 'assistant' - content String @db.Text - metadata Json? // Store verse references, etc. - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - - @@index([userId, createdAt]) -} - -model Bookmark { - id String @id @default(uuid()) - userId String - verseId String - note String? - color String @default("#FFD700") - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - verse BibleVerse @relation(fields: [verseId], references: [id]) - - @@unique([userId, verseId]) - @@index([userId]) -} - -model PrayerRequest { - id String @id @default(uuid()) - userId String? - content String @db.Text - isAnonymous Boolean @default(true) - prayerCount Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User? @relation(fields: [userId], references: [id]) - prayers Prayer[] - - @@index([createdAt]) -} - -model Prayer { - id String @id @default(uuid()) - requestId String - ipAddress String // For anonymous prayer counting - createdAt DateTime @default(now()) - - request PrayerRequest @relation(fields: [requestId], references: [id]) - - @@unique([requestId, ipAddress]) -} -``` - -### Step 1.3: Database Extensions and Functions - -**Database Initialization Script (scripts/init.sql):** -```sql --- Enable required extensions -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -CREATE EXTENSION IF NOT EXISTS "vector"; - --- Create cache table for Bible verses -CREATE UNLOGGED TABLE verse_cache ( - key VARCHAR(255) PRIMARY KEY, - value TEXT, - expires_at TIMESTAMP, - created_at TIMESTAMP DEFAULT NOW() -); - --- Create index for full-text search -CREATE INDEX verse_text_gin_idx ON "BibleVerse" USING gin(to_tsvector('english', text)); - --- Function for verse search -CREATE OR REPLACE FUNCTION search_verses(search_query TEXT, limit_count INT DEFAULT 10) -RETURNS TABLE( - verse_id UUID, - book_name TEXT, - chapter_num INT, - verse_num INT, - verse_text TEXT, - rank REAL -) AS $$ -BEGIN - RETURN QUERY - SELECT - v.id, - b.name, - c."chapterNum", - v."verseNum", - v.text, - ts_rank(to_tsvector('english', v.text), plainto_tsquery('english', search_query)) as rank - FROM "BibleVerse" v - JOIN "BibleChapter" c ON v."chapterId" = c.id - JOIN "BibleBook" b ON c."bookId" = b.id - WHERE to_tsvector('english', v.text) @@ plainto_tsquery('english', search_query) - ORDER BY rank DESC - LIMIT limit_count; -END; -$$ LANGUAGE plpgsql; - --- Session cleanup function -CREATE OR REPLACE FUNCTION cleanup_expired_sessions() -RETURNS void AS $$ -BEGIN - DELETE FROM "Session" WHERE "expiresAt" < NOW(); -END; -$$ LANGUAGE plpgsql; -``` - -## Phase 2: Core Backend Implementation (Days 4-8) - -### Step 2.1: Environment Configuration - -**.env.local file:** -```env -DATABASE_URL=postgresql://bible_admin:password@localhost:5432/bible_chat -NEXTAUTH_URL=http://localhost:3000 -NEXTAUTH_SECRET=generate-random-secret-here -AZURE_OPENAI_KEY=your-azure-key -AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com -AZURE_OPENAI_DEPLOYMENT=gpt-4 -OLLAMA_API_URL=http://your-ollama-server:11434 -JWT_SECRET=your-jwt-secret -REDIS_URL=redis://localhost:6379 -``` - -### Step 2.2: Database Connection and Utilities - -**src/lib/db.ts:** -```typescript -import { PrismaClient } from '@prisma/client' - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined -} - -export const prisma = globalForPrisma.prisma ?? new PrismaClient({ - log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], -}) - -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma -``` - -### Step 2.3: Authentication System - -**src/lib/auth/index.ts:** -```typescript -import bcrypt from 'bcryptjs' -import jwt from 'jsonwebtoken' -import { prisma } from '@/lib/db' - -export async function createUser(email: string, password: string, name?: string) { - const passwordHash = await bcrypt.hash(password, 10) - return prisma.user.create({ - data: { email, passwordHash, name } - }) -} - -export async function validateUser(email: string, password: string) { - const user = await prisma.user.findUnique({ where: { email } }) - if (!user) return null - - const isValid = await bcrypt.compare(password, user.passwordHash) - return isValid ? user : null -} - -export function generateToken(userId: string): string { - return jwt.sign({ userId }, process.env.JWT_SECRET!, { expiresIn: '7d' }) -} -``` - -### Step 2.4: Bible Data Import Script - -**scripts/import-bible.ts:** -```typescript -import { PrismaClient } from '@prisma/client' -import fs from 'fs/promises' -import path from 'path' - -const prisma = new PrismaClient() - -interface BibleData { - books: Array<{ - id: number - name: string - testament: string - chapters: Array<{ - number: number - verses: Array<{ - number: number - text: string - }> - }> - }> -} - -async function importBible() { - const dataPath = path.join(process.cwd(), 'data', 'bible-kjv.json') - const bibleData: BibleData = JSON.parse(await fs.readFile(dataPath, 'utf-8')) - - for (const book of bibleData.books) { - // Create book - await prisma.bibleBook.create({ - data: { - id: book.id, - name: book.name, - testament: book.testament, - orderNum: book.id - } - }) - - // Create chapters and verses - for (const chapter of book.chapters) { - const createdChapter = await prisma.bibleChapter.create({ - data: { - bookId: book.id, - chapterNum: chapter.number - } - }) - - // Bulk create verses - await prisma.bibleVerse.createMany({ - data: chapter.verses.map(verse => ({ - chapterId: createdChapter.id, - verseNum: verse.number, - text: verse.text, - version: 'KJV' - })) - }) - } - } -} - -importBible() - .then(() => console.log('Bible import completed')) - .catch(console.error) - .finally(() => prisma.$disconnect()) -``` - -## Phase 3: API Routes Implementation (Days 9-12) - -### Step 3.1: Authentication API Routes - -**src/app/api/auth/register/route.ts:** -```typescript -import { NextResponse } from 'next/server' -import { createUser, generateToken } from '@/lib/auth' -import { prisma } from '@/lib/db' - -export async function POST(request: Request) { - try { - const { email, password, name } = await request.json() - - // Validation - if (!email || !password) { - return NextResponse.json({ error: 'Email and password required' }, { status: 400 }) - } - - // Check if user exists - const existing = await prisma.user.findUnique({ where: { email } }) - if (existing) { - return NextResponse.json({ error: 'User already exists' }, { status: 409 }) - } - - // Create user - const user = await createUser(email, password, name) - const token = generateToken(user.id) - - // Create session - await prisma.session.create({ - data: { - userId: user.id, - token, - expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) - } - }) - - return NextResponse.json({ user: { id: user.id, email: user.email, name: user.name }, token }) - } catch (error) { - return NextResponse.json({ error: 'Server error' }, { status: 500 }) - } -} -``` - -### Step 3.2: Bible API Routes - -**src/app/api/bible/search/route.ts:** -```typescript -import { NextResponse } from 'next/server' -import { prisma } from '@/lib/db' - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - const query = searchParams.get('q') - const limit = parseInt(searchParams.get('limit') || '10') - - if (!query) { - return NextResponse.json({ error: 'Query required' }, { status: 400 }) - } - - // Use raw SQL for full-text search - const results = await prisma.$queryRaw` - SELECT * FROM search_verses(${query}, ${limit}) - ` - - return NextResponse.json({ results }) -} -``` - -### Step 3.3: AI Chat API Integration - -**src/lib/ai/azure-openai.ts:** -```typescript -import { AzureOpenAI } from 'openai' - -const client = new AzureOpenAI({ - apiKey: process.env.AZURE_OPENAI_KEY!, - apiVersion: '2024-02-01', - endpoint: process.env.AZURE_OPENAI_ENDPOINT!, -}) - -export async function generateChatResponse( - messages: Array<{ role: string; content: string }>, - verseContext?: string -) { - const systemPrompt = `You are a helpful Bible study assistant. Always provide scripture references for your answers. ${verseContext ? `Context: ${verseContext}` : ''}` - - const response = await client.chat.completions.create({ - model: process.env.AZURE_OPENAI_DEPLOYMENT!, - messages: [ - { role: 'system', content: systemPrompt }, - ...messages - ], - temperature: 0.7, - max_tokens: 1000 - }) - - return response.choices[0].message.content -} -``` - -**src/lib/ai/embeddings.ts:** -```typescript -export async function generateEmbedding(text: string): Promise { - const response = await fetch(`${process.env.OLLAMA_API_URL}/api/embeddings`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'nomic-embed-text', - prompt: text - }) - }) - - const data = await response.json() - return data.embedding -} -``` - -### Step 3.4: WebSocket Server for Real-time Features - -**src/lib/websocket/server.ts:** -```typescript -import { Server } from 'socket.io' -import { createServer } from 'http' -import { parse } from 'url' -import next from 'next' - -const dev = process.env.NODE_ENV !== 'production' -const app = next({ dev }) -const handle = app.getRequestHandler() - -app.prepare().then(() => { - const server = createServer((req, res) => { - const parsedUrl = parse(req.url!, true) - handle(req, res, parsedUrl) - }) - - const io = new Server(server, { - cors: { - origin: process.env.NEXTAUTH_URL, - methods: ['GET', 'POST'] - } - }) - - io.on('connection', (socket) => { - console.log('Client connected:', socket.id) - - // Join prayer room - socket.on('join-prayer-room', () => { - socket.join('prayers') - }) - - // Handle new prayer - socket.on('new-prayer', async (data) => { - // Save to database - // Broadcast to all in prayer room - io.to('prayers').emit('prayer-added', data) - }) - - // Handle prayer count update - socket.on('pray-for', async (requestId) => { - // Update count in database - // Broadcast updated count - io.to('prayers').emit('prayer-count-updated', { requestId, count: newCount }) - }) - - socket.on('disconnect', () => { - console.log('Client disconnected:', socket.id) - }) - }) - - server.listen(3001, () => { - console.log('WebSocket server running on port 3001') - }) -}) -``` - -## Phase 4: Frontend Implementation (Days 13-18) - -Primary UI Language: Romanian -Secondary UI Language: English - -### Step 4.1: Component Library Setup - -**Install UI Dependencies:** -```bash -npm install @radix-ui/react-dropdown-menu @radix-ui/react-dialog -npm install @radix-ui/react-tabs @radix-ui/react-toast -npm install socket.io-client zustand -npm install react-markdown remark-gfm -``` - -### Step 4.2: Global State Management - -**src/lib/store/index.ts:** -```typescript -import { create } from 'zustand' -import { persist } from 'zustand/middleware' - -interface AppState { - user: User | null - theme: 'light' | 'dark' - fontSize: 'small' | 'medium' | 'large' - currentBook: number - currentChapter: number - bookmarks: Bookmark[] - setUser: (user: User | null) => void - setTheme: (theme: 'light' | 'dark') => void - setFontSize: (size: 'small' | 'medium' | 'large') => void -} - -export const useStore = create()( - persist( - (set) => ({ - user: null, - theme: 'light', - fontSize: 'medium', - currentBook: 1, - currentChapter: 1, - bookmarks: [], - setUser: (user) => set({ user }), - setTheme: (theme) => set({ theme }), - setFontSize: (fontSize) => set({ fontSize }), - }), - { - name: 'bible-chat-storage', - } - ) -) -``` - -### Step 4.3: Main Layout Component - -**src/app/layout.tsx:** -```typescript -import './globals.css' -import { Providers } from '@/components/providers' -import { Navigation } from '@/components/navigation' - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - - - - {children} - - - - - ) -} -``` - -### Step 4.4: Bible Reader Component - -**src/components/bible/reader.tsx:** -```typescript -'use client' - -import { useState, useEffect } from 'react' -import { useStore } from '@/lib/store' - -export function BibleReader() { - const { currentBook, currentChapter } = useStore() - const [verses, setVerses] = useState([]) - const [loading, setLoading] = useState(true) - - useEffect(() => { - fetchChapter(currentBook, currentChapter) - }, [currentBook, currentChapter]) - - async function fetchChapter(bookId: number, chapterNum: number) { - setLoading(true) - const res = await fetch(`/api/bible/chapter?book=${bookId}&chapter=${chapterNum}`) - const data = await res.json() - setVerses(data.verses) - setLoading(false) - } - - const handleVerseClick = async (verseId: string) => { - // Add to bookmarks - await fetch('/api/bookmarks', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ verseId }) - }) - } - - if (loading) return Loading... - - return ( - - {verses.map((verse: any) => ( - handleVerseClick(verse.id)} - > - {verse.verseNum} - {verse.text} - - ))} - - ) -} -``` - -### Step 4.5: AI Chat Interface - -**src/components/chat/chat-interface.tsx:** -```typescript -'use client' - -import { useState, useRef, useEffect } from 'react' -import { Send } from 'lucide-react' - -export function ChatInterface() { - const [messages, setMessages] = useState([]) - const [input, setInput] = useState('') - const [loading, setLoading] = useState(false) - const messagesEndRef = useRef(null) - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) - } - - useEffect(scrollToBottom, [messages]) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!input.trim() || loading) return - - const userMessage = { role: 'user', content: input } - setMessages(prev => [...prev, userMessage]) - setInput('') - setLoading(true) - - try { - const res = await fetch('/api/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - messages: [...messages, userMessage] - }) - }) - - const data = await res.json() - setMessages(prev => [...prev, { - role: 'assistant', - content: data.response - }]) - } catch (error) { - console.error('Chat error:', error) - } finally { - setLoading(false) - } - } - - return ( - - - {messages.map((msg, idx) => ( - - - {msg.content} - - - ))} - {loading && ( - - - - - - - - - - )} - - - - - - setInput(e.target.value)} - placeholder="Ask about the Bible..." - className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2" - disabled={loading} - /> - - - - - - - ) -} -``` - -### Step 4.6: Real-time Prayer Wall - -**src/components/prayer/prayer-wall.tsx:** -```typescript -'use client' - -import { useEffect, useState } from 'react' -import { io, Socket } from 'socket.io-client' - -export function PrayerWall() { - const [socket, setSocket] = useState(null) - const [prayers, setPrayers] = useState([]) - const [newPrayer, setNewPrayer] = useState('') - - useEffect(() => { - const socketInstance = io('http://localhost:3001') - setSocket(socketInstance) - - socketInstance.emit('join-prayer-room') - - socketInstance.on('prayer-added', (prayer) => { - setPrayers(prev => [prayer, ...prev]) - }) - - socketInstance.on('prayer-count-updated', ({ requestId, count }) => { - setPrayers(prev => prev.map(p => - p.id === requestId ? { ...p, prayerCount: count } : p - )) - }) - - // Load initial prayers - fetchPrayers() - - return () => { - socketInstance.disconnect() - } - }, []) - - const fetchPrayers = async () => { - const res = await fetch('/api/prayers') - const data = await res.json() - setPrayers(data.prayers) - } - - const handleSubmitPrayer = async (e: React.FormEvent) => { - e.preventDefault() - if (!newPrayer.trim()) return - - const res = await fetch('/api/prayers', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: newPrayer, isAnonymous: true }) - }) - - const prayer = await res.json() - socket?.emit('new-prayer', prayer) - setNewPrayer('') - } - - const handlePrayFor = (requestId: string) => { - socket?.emit('pray-for', requestId) - } - - return ( - - - setNewPrayer(e.target.value)} - placeholder="Share your prayer request..." - className="w-full p-3 border rounded-lg resize-none h-24" - /> - - Submit Prayer Request - - - - - {prayers.map((prayer: any) => ( - - {prayer.content} - - - {new Date(prayer.createdAt).toLocaleDateString()} - - handlePrayFor(prayer.id)} - className="flex items-center space-x-2 px-4 py-2 bg-purple-100 rounded-lg hover:bg-purple-200" - > - 🙏 - {prayer.prayerCount} prayers - - - - ))} - - - ) -} -``` - -## Phase 5: Testing Implementation (Days 19-20) - -### Step 5.1: Unit Tests Setup - -**jest.config.js:** -```javascript -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - testMatch: ['**/__tests__/**/*.test.ts', '**/__tests__/**/*.test.tsx'], - collectCoverageFrom: [ - 'src/**/*.{ts,tsx}', - '!src/**/*.d.ts', - ], -} -``` - -### Step 5.2: API Route Tests - -**tests/api/auth.test.ts:** -```typescript -import { createMocks } from 'node-mocks-http' -import { POST as registerHandler } from '@/app/api/auth/register/route' - -describe('/api/auth/register', () => { - test('creates new user successfully', async () => { - const { req, res } = createMocks({ - method: 'POST', - body: { - email: 'test@example.com', - password: 'SecurePass123!', - name: 'Test User' - }, - }) - - await registerHandler(req as any) - - expect(res._getStatusCode()).toBe(200) - const json = JSON.parse(res._getData()) - expect(json).toHaveProperty('token') - expect(json.user.email).toBe('test@example.com') - }) - - test('rejects duplicate email', async () => { - // First registration - const { req: req1 } = createMocks({ - method: 'POST', - body: { - email: 'duplicate@example.com', - password: 'Pass123!', - }, - }) - await registerHandler(req1 as any) - - // Duplicate attempt - const { req: req2, res: res2 } = createMocks({ - method: 'POST', - body: { - email: 'duplicate@example.com', - password: 'Pass123!', - }, - }) - - await registerHandler(req2 as any) - expect(res2._getStatusCode()).toBe(409) - }) -}) -``` - -### Step 5.3: Component Tests - -**tests/components/bible-reader.test.tsx:** -```typescript -import { render, screen, waitFor } from '@testing-library/react' -import { BibleReader } from '@/components/bible/reader' - -jest.mock('@/lib/store', () => ({ - useStore: () => ({ - currentBook: 1, - currentChapter: 1, - }), -})) - -describe('BibleReader', () => { - test('renders verses correctly', async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve({ - verses: [ - { id: '1', verseNum: 1, text: 'In the beginning...' }, - { id: '2', verseNum: 2, text: 'And the earth was...' }, - ], - }), - }) - ) as jest.Mock - - render() - - await waitFor(() => { - expect(screen.getByText(/In the beginning/)).toBeInTheDocument() - }) - }) -}) -``` - -## Phase 6: Deployment Configuration (Days 21-22) - -### Step 6.1: Production Docker Configuration - -**docker/Dockerfile.prod:** -```dockerfile -FROM node:20-alpine AS builder -WORKDIR /app -COPY package*.json ./ -RUN npm ci -COPY . . -RUN npm run build - -FROM node:20-alpine AS runner -WORKDIR /app -ENV NODE_ENV production - -RUN addgroup -g 1001 -S nodejs -RUN adduser -S nextjs -u 1001 - -COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs -EXPOSE 3000 -ENV PORT 3000 - -CMD ["node", "server.js"] -``` - -### Step 6.2: Nginx Configuration - -**docker/nginx/nginx.conf:** -```nginx -upstream app { - server app:3000; -} - -upstream websocket { - server app:3001; -} - -server { - listen 80; - server_name _; - - client_max_body_size 10M; - - location / { - proxy_pass http://app; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /socket.io/ { - proxy_pass http://websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -### Step 6.3: Production Docker Compose - -**docker-compose.prod.yml:** -```yaml -version: '3.8' - -services: - postgres: - image: pgvector/pgvector:pg16 - restart: always - environment: - POSTGRES_DB: bible_chat - POSTGRES_USER: bible_admin - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - bible_network - - app: - build: - context: . - dockerfile: docker/Dockerfile.prod - restart: always - environment: - DATABASE_URL: postgresql://bible_admin:${DB_PASSWORD}@postgres:5432/bible_chat - AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY} - AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} - AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT} - OLLAMA_API_URL: ${OLLAMA_API_URL} - JWT_SECRET: ${JWT_SECRET} - NEXTAUTH_URL: ${NEXTAUTH_URL} - NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} - depends_on: - - postgres - networks: - - bible_network - - nginx: - image: nginx:alpine - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf - - ./certbot/conf:/etc/letsencrypt - - ./certbot/www:/var/www/certbot - depends_on: - - app - networks: - - bible_network - -networks: - bible_network: - driver: bridge - -volumes: - postgres_data: -``` - -## Phase 7: Performance Optimization (Days 23-24) - -### Step 7.1: Database Optimization - -**scripts/optimize-db.sql:** -```sql --- Create materialized view for popular verses -CREATE MATERIALIZED VIEW popular_verses AS -SELECT - v.id, - v.text, - b.name as book_name, - c."chapterNum", - v."verseNum", - COUNT(bm.id) as bookmark_count -FROM "BibleVerse" v -JOIN "BibleChapter" c ON v."chapterId" = c.id -JOIN "BibleBook" b ON c."bookId" = b.id -LEFT JOIN "Bookmark" bm ON v.id = bm."verseId" -GROUP BY v.id, v.text, b.name, c."chapterNum", v."verseNum" -ORDER BY bookmark_count DESC -LIMIT 100; - --- Refresh materialized view periodically -CREATE OR REPLACE FUNCTION refresh_popular_verses() -RETURNS void AS $$ -BEGIN - REFRESH MATERIALIZED VIEW CONCURRENTLY popular_verses; -END; -$$ LANGUAGE plpgsql; - --- Create indexes for performance -CREATE INDEX idx_chat_messages_user_created ON "ChatMessage"("userId", "createdAt" DESC); -CREATE INDEX idx_bookmarks_user ON "Bookmark"("userId"); -CREATE INDEX idx_reading_history_user ON "ReadingHistory"("userId", "viewedAt" DESC); - --- Partition chat messages by month for better performance -CREATE TABLE "ChatMessage_2024_01" PARTITION OF "ChatMessage" -FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); -``` - -### Step 7.2: Caching Layer - -**src/lib/cache/index.ts:** -```typescript -import { prisma } from '@/lib/db' - -export class CacheManager { - static async get(key: string): Promise { - const result = await prisma.$queryRaw<{ value: string }[]>` - SELECT value FROM verse_cache - WHERE key = ${key} - AND expires_at > NOW() - LIMIT 1 - ` - return result[0]?.value || null - } - - static async set(key: string, value: string, ttl: number = 3600) { - const expiresAt = new Date(Date.now() + ttl * 1000) - await prisma.$executeRaw` - INSERT INTO verse_cache (key, value, expires_at) - VALUES (${key}, ${value}, ${expiresAt}) - ON CONFLICT (key) DO UPDATE - SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at - ` - } - - static async invalidate(pattern: string) { - await prisma.$executeRaw` - DELETE FROM verse_cache WHERE key LIKE ${pattern} - ` - } -} -``` - -## Phase 8: Security Implementation (Day 25) - -### Step 8.1: Security Middleware - -**src/middleware.ts:** -```typescript -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import { verifyToken } from '@/lib/auth' - -export async function middleware(request: NextRequest) { - // Rate limiting - const ip = request.ip || 'unknown' - const rateLimit = await checkRateLimit(ip) - - if (!rateLimit.allowed) { - return new NextResponse('Too Many Requests', { status: 429 }) - } - - // Protected routes - if (request.nextUrl.pathname.startsWith('/api/protected')) { - const token = request.headers.get('authorization')?.replace('Bearer ', '') - - if (!token) { - return new NextResponse('Unauthorized', { status: 401 }) - } - - try { - const payload = await verifyToken(token) - // Add user to headers - const requestHeaders = new Headers(request.headers) - requestHeaders.set('x-user-id', payload.userId) - - return NextResponse.next({ - request: { - headers: requestHeaders, - }, - }) - } catch (error) { - return new NextResponse('Invalid token', { status: 401 }) - } - } - - return NextResponse.next() -} - -export const config = { - matcher: ['/api/:path*'], -} - -async function checkRateLimit(ip: string): Promise<{ allowed: boolean }> { - // Implement rate limiting logic using database - // Store request counts in database with TTL - return { allowed: true } -} -``` - -### Step 8.2: Input Validation - -**src/lib/validation/index.ts:** -```typescript -import { z } from 'zod' - -export const userSchema = z.object({ - email: z.string().email(), - password: z.string().min(8).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/), - name: z.string().min(2).max(50).optional(), -}) - -export const chatMessageSchema = z.object({ - content: z.string().min(1).max(1000), - verseContext: z.string().optional(), -}) - -export const prayerRequestSchema = z.object({ - content: z.string().min(10).max(500), - isAnonymous: z.boolean().default(true), -}) -``` - -## Deployment Instructions - -### Step 1: Server Setup -```bash -# Update system -sudo apt update && sudo apt upgrade -y - -# Install Docker -curl -fsSL https://get.docker.com -o get-docker.sh -sudo sh get-docker.sh - -# Install Docker Compose -sudo apt install docker-compose -y - -# Setup firewall -sudo ufw allow 22 -sudo ufw allow 80 -sudo ufw allow 443 -sudo ufw enable -``` - -### Step 2: Deploy Application -```bash -# Clone repository -git clone https://github.com/yourusername/bible-chat-app.git -cd bible-chat-app - -# Create .env.production file -cp .env.example .env.production -# Edit with your production values - -# Build and start -docker-compose -f docker-compose.prod.yml up -d - -# Run database migrations -docker-compose -f docker-compose.prod.yml exec app npx prisma migrate deploy - -# Import Bible data -docker-compose -f docker-compose.prod.yml exec app npm run import-bible -``` - -### Step 3: SSL Setup (Optional) -```bash -# Install Certbot -sudo apt install certbot python3-certbot-nginx - -# Get SSL certificate -sudo certbot --nginx -d yourdomain.com -``` - -## Monitoring and Maintenance - -### Health Check Endpoint -**src/app/api/health/route.ts:** -```typescript -export async function GET() { - const checks = { - database: await checkDatabase(), - cache: await checkCache(), - ai: await checkAIService(), - } - - const healthy = Object.values(checks).every(v => v) - - return NextResponse.json( - { status: healthy ? 'healthy' : 'unhealthy', checks }, - { status: healthy ? 200 : 503 } - ) -} -``` - -### Backup Script -```bash -#!/bin/bash -# backup.sh -DATE=$(date +%Y%m%d_%H%M%S) -docker-compose -f docker-compose.prod.yml exec -T postgres pg_dump -U bible_admin bible_chat > backup_$DATE.sql -# Upload to S3 or other storage -``` - -## Performance Benchmarks - -- Page Load: < 2 seconds -- API Response: < 500ms (cached), < 2s (database) -- AI Chat Response: < 5 seconds -- WebSocket Latency: < 100ms -- Database Queries: < 50ms (with indexes) - -## Scalability Considerations - -1. **Horizontal Scaling**: Use Docker Swarm or Kubernetes -2. **Database Replication**: PostgreSQL streaming replication -3. **CDN**: CloudFlare for static assets -4. **Load Balancing**: HAProxy or Nginx upstream -5. **Caching**: Redis cluster for session storage -6. **Message Queue**: RabbitMQ for background jobs - -## Security Checklist - -- [ ] Environment variables secured -- [ ] HTTPS enforced -- [ ] Rate limiting implemented -- [ ] Input validation on all endpoints -- [ ] SQL injection prevention (Prisma) -- [ ] XSS protection headers -- [ ] CSRF tokens -- [ ] Regular dependency updates -- [ ] Database backups configured -- [ ] Monitoring alerts setup - -## Final Notes - -This implementation plan provides a production-ready, scalable Biblical Guide application using only open-source technologies. The architecture is designed to handle 10,000+ concurrent users with proper scaling. Regular maintenance includes: - -1. Weekly database backups -2. Monthly security updates -3. Performance monitoring -4. User feedback implementation -5. AI model fine-tuning based on usage patterns - -The modular structure allows for easy feature additions and modifications as the application grows. \ No newline at end of file diff --git a/temp/biblical-guide-homepage.md b/temp/biblical-guide-homepage.md deleted file mode 100644 index ee3b935..0000000 --- a/temp/biblical-guide-homepage.md +++ /dev/null @@ -1,304 +0,0 @@ -# BiblicalGuide Homepage Sections - -## Current Sections ✓ -1. Hero/CTA (Read Bible & AI Chat) -2. Features Overview -3. Basic Stats - -## Recommended Additional Sections (In Order) - -### 1. **Hero Section Enhancement** (Update Current) -``` -Headline: "Your Personal Biblical Guide" -Subheadline: "Explore Scripture with AI-Powered Insights in [Language]" - -[Start Reading] [Chat with AI Guide] - -Live Counter: "Join 2,847 believers studying God's word right now" -``` - -### 2. **Interactive Demo Section** 🌟 -``` -Title: "See It In Action" - -[Live Chat Widget Preview] -User: "What does the Bible say about hope?" -AI: "The Bible offers many encouraging verses about hope..." - -[Try It Yourself - No Sign Up Required] -``` -**Why:** Reduce friction - let users experience the AI immediately without commitment - -### 3. **Daily Verse Section** 📖 -``` -Today's Verse - January 15, 2024 - -"For I know the plans I have for you," declares the Lord, -"plans to prosper you and not to harm you, -plans to give you hope and a future." -- Jeremiah 29:11 - -[💬 Discuss This Verse] [🔖 Save] [📤 Share] - -Tomorrow: Get daily verses delivered to your inbox [Subscribe] -``` -**Why:** Gives immediate value and a reason to return daily - -### 4. **Popular Questions Carousel** 💭 -``` -What Others Are Asking - -[< ] "What does the Bible say about anxiety?" - "Understanding God's plan for my life" - "How to pray effectively" - "Finding peace in difficult times" [ >] - -[Ask Your Own Question →] -``` -**Why:** Shows real use cases and inspires engagement - -### 5. **How It Works** (3 Steps) 🎯 -``` -Start Your Biblical Journey - -1. Ask Any Question - Type or speak your spiritual questions - [Icon: Chat bubble] - -2. Get Biblical Answers - Receive verses and insights instantly - [Icon: Book with sparkle] - -3. Grow in Faith - Save insights, track your journey - [Icon: Growth chart] - -[Get Started Free →] -``` -**Why:** Reduces complexity anxiety for non-tech users - -### 6. **Community Prayer Wall** (Live) 🙏 -``` -Prayer Requests From Our Community - -"Please pray for my mother's health..." - 2 min ago -[🙏 32 Praying] - -"Seeking guidance for my job interview..." - 15 min ago -[🙏 47 Praying] - -"Thankful for answered prayers!" - 1 hour ago -[❤️ 89 Celebrating] - -[Share Your Prayer Request] [View All Prayers] -``` -**Why:** Creates immediate sense of community and belonging - -### 7. **Trust Indicators Section** ✅ -``` -Trusted by Believers Worldwide - -✓ Theologically Verified - Reviewed by seminary professors and pastors - -✓ 100% Private & Secure - Your spiritual journey stays between you and God - -✓ Always Free Core Features - Essential tools available to everyone - -✓ Multi-Language Support - Available in 25+ languages - -[Logos of: Christianity Today | Bible Society | Local Churches] -``` -**Why:** Builds credibility, especially important for faith-based apps - -### 8. **Use Cases / Who It's For** 👥 -``` -Perfect For Every Believer - -[Tab: New Christians] -Start your faith journey with gentle guidance -- Basic Bible navigation -- Simple explanations -- Foundational topics - -[Tab: Bible Students] -Deep dive into Scripture -- Original language insights -- Historical context -- Cross-references - -[Tab: Parents & Teachers] -Share faith with the next generation -- Age-appropriate answers -- Discussion guides -- Family devotionals - -[Tab: Pastors & Leaders] -Enhance your ministry -- Sermon preparation -- Counseling support -- Quick references -``` -**Why:** Helps visitors self-identify and see personalized value - -### 9. **Testimonials with Context** 💬 -``` -Real Stories from Our Community - -"As a new mom with little time, the AI chat helps me stay -connected to God's word during 3am feedings." -- Sarah M., Mother of Two | Using for 3 months - -"I was skeptical about AI and faith, but the Biblical accuracy -and thoughtful responses exceeded my expectations." -- Pastor David K., Baptist Church | Verified Minister - -"Finally, a Bible app that speaks my language - literally! -The Portuguese support is perfect." -- João S., Brazil | International User - -[Share Your Story] -``` -**Why:** Social proof with relatable contexts - -### 10. **FAQ Section** (Expandable) ❓ -``` -Common Questions - -▼ Is the AI theologically accurate? -▼ Is it really free? -▼ What languages are supported? -▼ Can I use it offline? -▼ Is my data private? -▼ What Bible versions are available? - -[Contact Support] [View All FAQs] -``` -**Why:** Addresses objections before they become barriers - -### 11. **Mobile App CTA** (If Applicable) 📱 -``` -Take Your Faith Everywhere - -[App Store] [Google Play] - -⭐⭐⭐⭐⭐ 4.8 rating | 10K+ downloads - -QR Code: [Scan to Download] -``` -**Why:** Many users prefer apps for daily spiritual practices - -### 12. **Footer Enhancement** (Update Current) -``` -Newsletter Signup: -"Daily Wisdom in Your Inbox" -[Email] [Subscribe] - -Quick Links: -About | Blog | Support | API Docs - -Legal: -Terms | Privacy | Cookie Policy | GDPR - -Languages: [EN] [ES] [PT] [FR] [DE] [+20 more] - -Social: [Facebook] [Instagram] [YouTube] [Twitter] - -© 2024 BiblicalGuide - Made with ❤️ and 🙏 -``` - -## Optional Advanced Sections - -### **Interactive Bible Map** 🗺️ -- Visual journey through Biblical locations -- "Explore where Jesus walked" -- Great for engagement but development-heavy - -### **Live Study Groups** 👥 -- "3 groups studying Romans right now" -- "Join a study group" CTA -- Requires more complex infrastructure - -### **Verse of the Day Widget** -- Embeddable widget code -- "Add to your website" -- Good for viral growth - -## Homepage Section Priority - -### Must Have (Launch): -1. Enhanced Hero -2. Interactive Demo -3. How It Works -4. Trust Indicators -5. Enhanced Footer - -### Should Have (Week 2): -6. Daily Verse -7. Popular Questions -8. Community Prayer Wall -9. FAQ - -### Nice to Have (Month 2): -10. Use Cases -11. Testimonials -12. Mobile App CTA - -## Design Principles - -### Above the Fold: -- **Hero with clear value prop** -- **Two primary CTAs** (Read & Chat) -- **Trust indicator** (user count or endorsement) -- **Interactive demo** teaser - -### Middle Sections: -- **Build trust** progressively -- **Show community** presence -- **Address objections** naturally -- **Provide value** immediately - -### Bottom Sections: -- **Capture emails** for retention -- **Social proof** for hesitant users -- **Resources** for power users -- **Legal/trust** information - -## A/B Testing Opportunities - -1. **Hero Headlines** - - "Your Personal Biblical Guide" - - "Chat with Scripture" - - "AI-Powered Bible Study" - -2. **Primary CTA** - - "Start Free" - - "Chat Now" - - "Explore Scripture" - -3. **Demo Placement** - - Above fold vs below - - Auto-play vs click-to-start - -4. **Social Proof Type** - - User count vs testimonials - - Ratings vs endorsements - -## Mobile Considerations - -- **Collapse sections** to accordions -- **Sticky CTA** at bottom -- **Swipeable** testimonials -- **Simplified** prayer wall -- **Touch-optimized** demo - -## Performance Tips - -- **Lazy load** below-fold sections -- **Static cache** daily verse -- **WebSocket** for live prayer wall -- **Skeleton screens** while loading -- **Progressive enhancement** for demo \ No newline at end of file diff --git a/temp/multi-language-implementation-plan.md b/temp/multi-language-implementation-plan.md deleted file mode 100644 index f5cf1d3..0000000 --- a/temp/multi-language-implementation-plan.md +++ /dev/null @@ -1,212 +0,0 @@ -# Multi-Language Support Implementation Plan - -## Overview -Add comprehensive multi-language support to the Ghid Biblic application, starting with English as the second language alongside Romanian. - -## Current State -- **Database**: Already supports multiple languages (`lang` field) and translations (`translation` field) -- **Frontend**: Hardcoded Romanian interface -- **Vector Search**: Romanian-only search logic -- **Bible Data**: Only Romanian (FIDELA) version imported - -## Implementation Phases - -### Phase 1: Core Infrastructure -1. **Install i18n Framework** - - Add `next-intl` for Next.js internationalization - - Configure locale routing (`/ro/`, `/en/`) - - Set up translation file structure - -2. **Language Configuration** - - Create language detection and switching logic - - Add language persistence (localStorage/cookies) - - Configure default language fallbacks - -3. **Translation Files Structure** - ``` - messages/ - ├── ro.json (Romanian - existing content) - ├── en.json (English translations) - └── common.json (shared terms) - ``` - -### Phase 2: UI Internationalization -1. **Navigation Component** - - Translate all menu items and labels - - Add language switcher dropdown - - Update routing for locale-aware navigation - -2. **Chat Interface** - - Translate all UI text and prompts - - Add suggested questions per language - - Update loading states and error messages - -3. **Page Content** - - Home page (`/` → `/[locale]/`) - - Bible browser (`/bible` → `/[locale]/bible`) - - Search page (`/search` → `/[locale]/search`) - - Prayer requests (`/prayers` → `/[locale]/prayers`) - -### Phase 3: Backend Localization -1. **Vector Search Updates** - - Modify search functions to filter by language - - Add language parameter to search APIs - - Update hybrid search for language-specific full-text search - -2. **Chat API Enhancement** - - Language-aware Bible verse retrieval - - Localized AI response prompts - - Language-specific fallback responses - -3. **API Route Updates** - - Add locale parameter to all API endpoints - - Update error responses for each language - - Configure language-specific search configurations - -### Phase 4: Bible Data Management -1. **English Bible Import** - - Source: API.Bible or public domain English Bible (KJV/ESV) - - Adapt existing import script for English - - Generate English embeddings using Azure OpenAI - -2. **Language-Aware Bible Browser** - - Add language selector in Bible interface - - Filter books/chapters/verses by selected language - - Show parallel verses when both languages available - -### Phase 5: Enhanced Features -1. **Parallel Bible View** - - Side-by-side Romanian/English verse display - - Cross-reference linking between translations - - Language comparison in search results - -2. **Smart Language Detection** - - Auto-detect query language in chat - - Suggest language switch based on user input - - Mixed-language search capabilities - -3. **Advanced Search Features** - - Cross-language semantic search - - Translation comparison tools - - Language-specific biblical term glossaries - -## Technical Implementation Details - -### Routing Structure -``` -Current: /page -New: /[locale]/page - -Examples: -- /ro/biblia (Romanian Bible) -- /en/bible (English Bible) -- /ro/rugaciuni (Romanian Prayers) -- /en/prayers (English Prayers) -``` - -### Database Schema Changes -**No changes needed** - current schema already supports: -- Multiple languages via `lang` field -- Multiple translations via `translation` field -- Unique constraints per translation/language - -### Vector Search Updates -```typescript -// Current -searchBibleHybrid(query: string, limit: number) - -// Enhanced -searchBibleHybrid(query: string, language: string, limit: number) -``` - -### Translation File Structure -```json -// messages/en.json -{ - "navigation": { - "home": "Home", - "bible": "Bible", - "prayers": "Prayers", - "search": "Search" - }, - "chat": { - "placeholder": "Ask your biblical question...", - "suggestions": [ - "What does the Bible say about love?", - "Explain the parable of the sower", - "What are the fruits of the Spirit?" - ] - } -} -``` - -### Language Switcher Component -- Dropdown in navigation header -- Flag icons for visual identification -- Persist language choice across sessions -- Redirect to equivalent page in new language - -## Dependencies to Add -```json -{ - "next-intl": "^3.x", - "@formatjs/intl-localematcher": "^0.x", - "negotiator": "^0.x" -} -``` - -## File Structure Changes -``` -app/ -├── [locale]/ -│ ├── page.tsx -│ ├── bible/ -│ ├── prayers/ -│ ├── search/ -│ └── layout.tsx -├── api/ (unchanged) -└── globals.css - -messages/ -├── en.json -├── ro.json -└── index.ts - -components/ -├── language-switcher.tsx -├── navigation.tsx (updated) -└── chat/ (updated) -``` - -## Testing Strategy -1. **Unit Tests**: Translation loading and language switching -2. **Integration Tests**: API endpoints with locale parameters -3. **E2E Tests**: Complete user flows in both languages -4. **Performance Tests**: Vector search with language filtering - -## Rollout Plan -1. **Development**: Implement Phase 1-3 (core infrastructure and UI) -2. **Testing**: Deploy to staging with Romanian/English support -3. **Beta Release**: Limited user testing with feedback collection -4. **Production**: Full release with both languages -5. **Future**: Add additional languages based on user demand - -## Estimated Timeline -- **Phase 1-2**: 2-3 days (i18n setup and UI translation) -- **Phase 3**: 1-2 days (backend localization) -- **Phase 4**: 2-3 days (English Bible import and embeddings) -- **Phase 5**: 3-4 days (enhanced features) -- **Total**: 8-12 days for complete implementation - -## Success Metrics -- Language switching works seamlessly -- Vector search returns accurate results in both languages -- AI chat responses are contextually appropriate per language -- User can browse Bible in preferred language -- Performance remains optimal with language filtering - -## Future Considerations -- Spanish, French, German language support -- Regional dialect variations -- Audio Bible integration per language -- Collaborative translation features for community contributions \ No newline at end of file
{prayer.content}