Add Ollama embedding support and improve prayer system with public/private visibility
- Add Ollama fallback support in vector search with Azure OpenAI as primary - Enhance prayer system with public/private visibility options and language filtering - Update OG image to use new biblical-guide-og-image.png - Improve prayer request management with better categorization - Remove deprecated ingest_json_pgvector.py script 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
useTheme,
|
||||
@@ -27,6 +24,10 @@ import {
|
||||
Tabs,
|
||||
Tab,
|
||||
FormControlLabel,
|
||||
FormControl,
|
||||
Select,
|
||||
Checkbox,
|
||||
SelectChangeEvent,
|
||||
Switch,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
Edit,
|
||||
Login,
|
||||
} from '@mui/icons-material'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslations, useLocale, useFormatter } from 'next-intl'
|
||||
import { useAuth } from '@/hooks/use-auth'
|
||||
|
||||
@@ -55,6 +56,9 @@ interface PrayerRequest {
|
||||
timestamp: Date
|
||||
prayerCount: number
|
||||
isPrayedFor: boolean
|
||||
isPublic: boolean
|
||||
language: string
|
||||
isOwner: boolean
|
||||
}
|
||||
|
||||
export default function PrayersPage() {
|
||||
@@ -72,10 +76,50 @@ export default function PrayersPage() {
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'personal',
|
||||
isPublic: false,
|
||||
})
|
||||
const [aiPrompt, setAiPrompt] = useState('')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [viewMode, setViewMode] = useState<'private' | 'public'>(user ? 'private' : 'public')
|
||||
const [selectedLanguages, setSelectedLanguages] = useState<string[]>([locale])
|
||||
|
||||
const languagesKey = useMemo(() => selectedLanguages.slice().sort().join(','), [selectedLanguages])
|
||||
const languageOptions = useMemo(() => ([
|
||||
{ value: 'en', label: t('languageFilter.options.en') },
|
||||
{ value: 'ro', label: t('languageFilter.options.ro') }
|
||||
]), [t])
|
||||
const languageLabelMap = useMemo(() => (
|
||||
languageOptions.reduce((acc, option) => {
|
||||
acc[option.value] = option.label
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
), [languageOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setViewMode(prev => (prev === 'private' ? prev : 'private'))
|
||||
} else {
|
||||
setViewMode('public')
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'public') {
|
||||
setSelectedLanguages(prev => {
|
||||
if (prev.includes(locale)) {
|
||||
return prev
|
||||
}
|
||||
return [...prev, locale]
|
||||
})
|
||||
}
|
||||
}, [locale, viewMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'public' && selectedLanguages.length === 0) {
|
||||
setSelectedLanguages([locale])
|
||||
}
|
||||
}, [viewMode, selectedLanguages, locale])
|
||||
|
||||
const categories = [
|
||||
{ value: 'personal', label: t('categories.personal'), color: 'primary' },
|
||||
@@ -88,6 +132,12 @@ export default function PrayersPage() {
|
||||
|
||||
// Fetch prayers from API
|
||||
const fetchPrayers = async () => {
|
||||
if (viewMode === 'private' && !user) {
|
||||
setPrayers([])
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
@@ -95,11 +145,25 @@ export default function PrayersPage() {
|
||||
params.append('category', selectedCategory)
|
||||
}
|
||||
params.append('limit', '50')
|
||||
if (user?.id) {
|
||||
params.append('userId', user.id)
|
||||
params.append('visibility', viewMode)
|
||||
|
||||
if (viewMode === 'public') {
|
||||
const languagesToQuery = selectedLanguages.length > 0 ? selectedLanguages : [locale]
|
||||
languagesToQuery.forEach(lang => params.append('languages', lang))
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/prayers?${params.toString()}`)
|
||||
const headers: Record<string, string> = {}
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/prayers?${params.toString()}`, {
|
||||
headers
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPrayers(data.prayers.map((prayer: any) => ({
|
||||
@@ -107,6 +171,9 @@ export default function PrayersPage() {
|
||||
timestamp: new Date(prayer.timestamp)
|
||||
})))
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
setPrayers([])
|
||||
}
|
||||
console.error('Failed to fetch prayers')
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -118,7 +185,7 @@ export default function PrayersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrayers()
|
||||
}, [selectedCategory, user])
|
||||
}, [selectedCategory, user, viewMode, languagesKey])
|
||||
|
||||
const handleGenerateAIPrayer = async () => {
|
||||
if (!aiPrompt.trim()) return
|
||||
@@ -144,7 +211,8 @@ export default function PrayersPage() {
|
||||
setNewPrayer({
|
||||
title: data.title || '',
|
||||
description: data.prayer || '',
|
||||
category: newPrayer.category
|
||||
category: newPrayer.category,
|
||||
isPublic: newPrayer.isPublic
|
||||
})
|
||||
setTabValue(0) // Switch to write tab to review generated prayer
|
||||
} else {
|
||||
@@ -157,43 +225,41 @@ export default function PrayersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleLanguageChange = (event: SelectChangeEvent<string[]>) => {
|
||||
const value = event.target.value
|
||||
const parsed = typeof value === 'string'
|
||||
? value.split(',')
|
||||
: (value as string[])
|
||||
|
||||
const uniqueValues = Array.from(new Set(parsed.filter(Boolean)))
|
||||
setSelectedLanguages(uniqueValues)
|
||||
}
|
||||
|
||||
const handleSubmitPrayer = async () => {
|
||||
if (!newPrayer.title.trim() || !newPrayer.description.trim()) return
|
||||
if (!user) return
|
||||
|
||||
const prayer: PrayerRequest = {
|
||||
id: Date.now().toString(),
|
||||
title: newPrayer.title,
|
||||
description: newPrayer.description,
|
||||
category: newPrayer.category,
|
||||
author: user.name || (locale === 'en' ? 'You' : 'Tu'),
|
||||
timestamp: new Date(),
|
||||
prayerCount: 0,
|
||||
isPrayedFor: false,
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken')
|
||||
const response = await fetch('/api/prayers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: newPrayer.title,
|
||||
description: newPrayer.description,
|
||||
category: newPrayer.category,
|
||||
isAnonymous: false
|
||||
isAnonymous: false,
|
||||
isPublic: newPrayer.isPublic,
|
||||
language: locale
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPrayers([{
|
||||
...data.prayer,
|
||||
timestamp: new Date(data.prayer.timestamp)
|
||||
}, ...prayers])
|
||||
setNewPrayer({ title: '', description: '', category: 'personal' })
|
||||
await fetchPrayers()
|
||||
setNewPrayer({ title: '', description: '', category: 'personal', isPublic: false })
|
||||
setAiPrompt('')
|
||||
setTabValue(0)
|
||||
setOpenDialog(false)
|
||||
@@ -341,6 +407,36 @@ export default function PrayersPage() {
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{viewMode === 'public' && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t('languageFilter.title')}
|
||||
</Typography>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
multiple
|
||||
value={selectedLanguages}
|
||||
onChange={handleLanguageChange}
|
||||
renderValue={(selected) =>
|
||||
(selected as string[])
|
||||
.map(code => languageLabelMap[code] || code.toUpperCase())
|
||||
.join(', ')
|
||||
}
|
||||
>
|
||||
{languageOptions.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Checkbox checked={selectedLanguages.includes(option.value)} />
|
||||
<ListItemText primary={option.label} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{t('languageFilter.helper')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
|
||||
{t('stats.title')}
|
||||
</Typography>
|
||||
@@ -355,6 +451,30 @@ export default function PrayersPage() {
|
||||
|
||||
{/* Prayer Requests */}
|
||||
<Box sx={{ flex: 1, width: { xs: '100%', md: '75%' } }}>
|
||||
{user && (
|
||||
<Tabs
|
||||
value={viewMode}
|
||||
onChange={(_, newValue) => setViewMode(newValue as 'private' | 'public')}
|
||||
sx={{ mb: 3 }}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="private" label={t('viewModes.private')} />
|
||||
<Tab value="public" label={t('viewModes.public')} />
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{viewMode === 'private' && (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
{t('alerts.privateInfo')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{viewMode === 'public' && !user && (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
{t('alerts.publicInfo')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Box>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
@@ -388,23 +508,43 @@ export default function PrayersPage() {
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
{prayers.map((prayer) => {
|
||||
{prayers.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{viewMode === 'private' ? t('empty.private') : t('empty.public')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : prayers.map((prayer) => {
|
||||
const categoryInfo = getCategoryInfo(prayer.category)
|
||||
const authorName = prayer.isOwner ? (locale === 'en' ? 'You' : 'Tu') : prayer.author
|
||||
const languageLabel = languageLabelMap[prayer.language] || prayer.language.toUpperCase()
|
||||
return (
|
||||
<Card key={prayer.id} sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<Typography variant="h6" component="h3">
|
||||
{prayer.title}
|
||||
</Typography>
|
||||
<Typography variant="h6" component="h3">
|
||||
{prayer.title}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1, mt: 1 }}>
|
||||
<Chip
|
||||
label={categoryInfo.label}
|
||||
color={categoryInfo.color as any}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={prayer.isPublic ? t('chips.public') : t('chips.private')}
|
||||
size="small"
|
||||
color={prayer.isPublic ? 'success' : 'default'}
|
||||
variant={prayer.isPublic ? 'filled' : 'outlined'}
|
||||
/>
|
||||
<Chip
|
||||
label={languageLabel}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
@@ -413,7 +553,7 @@ export default function PrayersPage() {
|
||||
<Person sx={{ fontSize: 16 }} />
|
||||
</Avatar>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{prayer.author}
|
||||
{authorName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
@@ -450,6 +590,7 @@ export default function PrayersPage() {
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<Share />}
|
||||
disabled={!prayer.isPublic}
|
||||
>
|
||||
{t('buttons.share')}
|
||||
</Button>
|
||||
@@ -602,6 +743,21 @@ export default function PrayersPage() {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={newPrayer.isPublic}
|
||||
onChange={(event) => setNewPrayer({ ...newPrayer, isPublic: event.target.checked })}
|
||||
/>
|
||||
}
|
||||
label={t('dialog.makePublic')}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
{newPrayer.isPublic ? t('dialog.visibilityPublic') : t('dialog.visibilityPrivate')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
|
||||
Reference in New Issue
Block a user