Fix candle layout, relax AI moderation, fix ReadableStream error
- Candles: clean grid layout with consistent sizing, no overlapping - AI moderation: much more lenient prompt - short descriptions, dates, locations are all valid memorial content - Fix ReadableStream error by clearing abort timeout after response Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -51,19 +51,32 @@ async function moderateWithAI(contributionId: number, content: string) {
|
|||||||
console.log(`[AI-Mod] No bad words, asking AI...`)
|
console.log(`[AI-Mod] No bad words, asking AI...`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prompt = `Ist dieser Text angemessen für eine Gedenkseite einer verstorbenen Großmutter?
|
const prompt = `Du prüfst Beiträge für eine Gedenkseite einer verstorbenen Großmutter (Maria Malejka).
|
||||||
|
|
||||||
"${content}"
|
Text: "${content}"
|
||||||
|
|
||||||
ERLAUBT: Liebe, Vermissen, Trauer, Erinnerungen, persönliche Geschichten, Beileidsbekundungen
|
WICHTIG: Sei SEHR großzügig! Die meisten Beiträge sind von Familienmitgliedern und Freunden.
|
||||||
VERBOTEN: Beleidigungen, Spam, Hassrede, Werbung, völlig zusammenhanglose oder sinnlose Texte ohne Bezug
|
|
||||||
|
ERLAUBT (immer appropriate=true):
|
||||||
|
- Kurze Beschreibungen wie "Hochzeit", "Geburtstag", "Urlaub in..." - das sind Erinnerungen!
|
||||||
|
- Namen, Orte, Daten - das sind Zeitstrahl-Einträge
|
||||||
|
- Alles was eine Erinnerung, ein Ereignis oder ein Lebensmoment sein könnte
|
||||||
|
- Persönliche Geschichten, Beileidsbekundungen, Liebe, Vermissen, Trauer
|
||||||
|
- Auch sehr kurze Texte oder einzelne Wörter sind OK wenn sie ein Ereignis beschreiben
|
||||||
|
|
||||||
|
NUR VERBOTEN (appropriate=false):
|
||||||
|
- Beleidigungen, Hassrede, Schimpfwörter
|
||||||
|
- Offensichtlicher Spam oder Werbung mit Links
|
||||||
|
- Komplett sinnloser Text (zufällige Buchstaben, Tastatur-Spam)
|
||||||
|
|
||||||
|
Im Zweifel: appropriate=true
|
||||||
|
|
||||||
Antworte NUR mit JSON:
|
Antworte NUR mit JSON:
|
||||||
{"appropriate": true} oder {"appropriate": false, "reason": "kurze Begründung"}
|
{"appropriate": true} oder {"appropriate": false, "reason": "kurze Begründung"}
|
||||||
JSON:`
|
JSON:`
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
setTimeout(() => controller.abort(), 15000)
|
const timeout = setTimeout(() => controller.abort(), 15000)
|
||||||
|
|
||||||
const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434'
|
const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434'
|
||||||
const res = await fetch(`${ollamaUrl}/api/generate`, {
|
const res = await fetch(`${ollamaUrl}/api/generate`, {
|
||||||
@@ -83,11 +96,13 @@ JSON:`
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
clearTimeout(timeout)
|
||||||
console.warn(`[AI-Mod] Ollama error: ${res.status}`)
|
console.warn(`[AI-Mod] Ollama error: ${res.status}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
clearTimeout(timeout)
|
||||||
const responseText = (data.response || '').trim()
|
const responseText = (data.response || '').trim()
|
||||||
console.log(`[AI-Mod] Response: "${responseText}"`)
|
console.log(`[AI-Mod] Response: "${responseText}"`)
|
||||||
|
|
||||||
|
|||||||
@@ -21,23 +21,23 @@ export async function POST(req: NextRequest) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'llama3.2:1b',
|
model: 'llama3.2:1b',
|
||||||
prompt: `Du bist ein Moderator für eine Gedenkseite. Prüfe, ob dieser Beitrag angemessen ist.
|
prompt: `Du prüfst Beiträge für eine Gedenkseite einer verstorbenen Großmutter (Maria Malejka). Sei SEHR großzügig!
|
||||||
|
|
||||||
Name: ${name || 'Anonym'}
|
Name: ${name || 'Anonym'}
|
||||||
Titel: ${title || 'Kein Titel'}
|
Titel: ${title || 'Kein Titel'}
|
||||||
Text: "${text}"
|
Text: "${text}"
|
||||||
|
|
||||||
Unangemessen sind:
|
ERLAUBT (immer appropriate=true):
|
||||||
- Spam, Werbung, Links zu Produkten
|
- Kurze Beschreibungen, Namen, Orte, Daten - das sind Erinnerungen!
|
||||||
- Beleidigungen, Hassrede
|
- Persönliche Geschichten, Beileidsbekundungen, Liebe, Vermissen, Trauer
|
||||||
- Völlig irrelevanter Inhalt
|
- Auch sehr kurze oder allgemeine Texte sind OK
|
||||||
- Unseriöse oder respektlose Inhalte
|
|
||||||
|
|
||||||
Angemessen sind:
|
NUR VERBOTEN (appropriate=false):
|
||||||
- Erinnerungen, Anekdoten
|
- Beleidigungen, Hassrede, Schimpfwörter
|
||||||
- Kondolenzen, Beileidsbekundungen
|
- Offensichtlicher Spam oder Werbung mit Links
|
||||||
- Persönliche Geschichten
|
- Komplett sinnloser Text (Tastatur-Spam)
|
||||||
- Emotionale oder traurige Texte
|
|
||||||
|
Im Zweifel: appropriate=true
|
||||||
|
|
||||||
Antworte NUR mit einem JSON-Objekt:
|
Antworte NUR mit einem JSON-Objekt:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,23 +89,20 @@ function CandleFlame({ size = 1, delay = 0 }: { size?: number; delay?: number })
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SingleCandle({ candle, index }: { candle: CandleData; index: number }) {
|
function SingleCandle({ candle, index }: { candle: CandleData; index: number }) {
|
||||||
// Generate consistent but varied properties based on candle ID
|
const seed = candle.id * 7919
|
||||||
const seed = candle.id * 7919 // Prime number for good distribution
|
const heightVariant = ((seed % 7) / 6) * 0.3 + 0.85 // 0.85 to 1.15 (subtler)
|
||||||
const sizeVariant = ((seed % 5) / 4) * 0.6 + 0.7 // 0.7 to 1.3
|
const hueShift = (seed % 14) - 7 // -7 to +7
|
||||||
const heightVariant = ((seed % 7) / 6) * 0.5 + 0.75 // 0.75 to 1.25
|
const brightnessShift = ((seed % 11) - 5) / 100
|
||||||
const hueShift = (seed % 20) - 10 // -10 to +10 hue shift
|
const rotation = ((seed % 7) - 3) / 3 // -1 to +1 degrees (subtler)
|
||||||
const brightnessShift = ((seed % 15) - 7) / 100 // -0.07 to +0.08 brightness
|
|
||||||
const rotation = ((seed % 11) - 5) / 2 // -2.5 to +2.5 degrees
|
|
||||||
|
|
||||||
// Calculate burn-down based on age
|
|
||||||
const createdTime = new Date(candle.created_at + 'Z').getTime()
|
const createdTime = new Date(candle.created_at + 'Z').getTime()
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const ageInHours = (now - createdTime) / (1000 * 60 * 60)
|
const ageInHours = (now - createdTime) / (1000 * 60 * 60)
|
||||||
const burnProgress = Math.min(ageInHours / 24, 0.4) // Burns down max 40% over 24 hours
|
const burnProgress = Math.min(ageInHours / 24, 0.4)
|
||||||
|
|
||||||
const candleHeight = 80 * heightVariant * (1 - burnProgress)
|
const candleHeight = 72 * heightVariant * (1 - burnProgress)
|
||||||
const candleWidth = 36 * sizeVariant
|
const candleWidth = 32
|
||||||
const flameSize = 1.0 * sizeVariant
|
const flameSize = 0.95
|
||||||
const delay = (index % 7) * 0.15
|
const delay = (index % 7) * 0.15
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,12 +111,12 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
transition={{ duration: 0.6, delay: Math.min(index * 0.08, 1.2) }}
|
transition={{ duration: 0.6, delay: Math.min(index * 0.08, 1.2) }}
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
transform: `rotate(${rotation}deg)`,
|
transform: `rotate(${rotation}deg)`,
|
||||||
filter: `brightness(${1 + brightnessShift})`,
|
filter: `brightness(${1 + brightnessShift})`,
|
||||||
marginLeft: index > 0 ? '-8px' : '0', // Slight overlap for natural clustering
|
width: 72,
|
||||||
}}
|
}}
|
||||||
className="group relative"
|
className="group relative"
|
||||||
>
|
>
|
||||||
@@ -141,7 +138,7 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 2,
|
width: 2,
|
||||||
height: 6 * sizeVariant,
|
height: 6,
|
||||||
background: 'linear-gradient(to bottom, #2b1a0a, #1a0f06)',
|
background: 'linear-gradient(to bottom, #2b1a0a, #1a0f06)',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
}}
|
}}
|
||||||
@@ -155,7 +152,7 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
|
|||||||
background: `linear-gradient(to bottom,
|
background: `linear-gradient(to bottom,
|
||||||
hsl(${35 + hueShift}, ${65 + hueShift}%, ${88 + brightnessShift * 10}%) 0%,
|
hsl(${35 + hueShift}, ${65 + hueShift}%, ${88 + brightnessShift * 10}%) 0%,
|
||||||
hsl(${32 + hueShift}, ${60 + hueShift}%, ${82 + brightnessShift * 10}%) 100%)`,
|
hsl(${32 + hueShift}, ${60 + hueShift}%, ${82 + brightnessShift * 10}%) 100%)`,
|
||||||
borderRadius: `${2 * sizeVariant}px ${2 * sizeVariant}px ${4 * sizeVariant}px ${4 * sizeVariant}px`,
|
borderRadius: '2px 2px 4px 4px',
|
||||||
boxShadow: `
|
boxShadow: `
|
||||||
inset 2px 0 4px rgba(255,255,255,${0.4 + brightnessShift}),
|
inset 2px 0 4px rgba(255,255,255,${0.4 + brightnessShift}),
|
||||||
inset -2px 0 6px rgba(0,0,0,${0.2 - brightnessShift}),
|
inset -2px 0 6px rgba(0,0,0,${0.2 - brightnessShift}),
|
||||||
@@ -165,21 +162,21 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Wax drips - more visible */}
|
{/* Wax drips */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: `${15 + (seed % 30)}%`,
|
left: `${15 + (seed % 30)}%`,
|
||||||
width: `${4 * sizeVariant}px`,
|
width: '4px',
|
||||||
height: `${16 * heightVariant}px`,
|
height: `${16 * heightVariant}px`,
|
||||||
background: `linear-gradient(to bottom,
|
background: `linear-gradient(to bottom,
|
||||||
hsl(${33 + hueShift}, ${60 + hueShift}%, ${82 + brightnessShift * 10}%) 0%,
|
hsl(${33 + hueShift}, ${60 + hueShift}%, ${82 + brightnessShift * 10}%) 0%,
|
||||||
hsl(${33 + hueShift}, ${55 + hueShift}%, ${85 + brightnessShift * 10}%) 50%,
|
hsl(${33 + hueShift}, ${55 + hueShift}%, ${85 + brightnessShift * 10}%) 50%,
|
||||||
transparent 100%)`,
|
transparent 100%)`,
|
||||||
borderRadius: `0 0 ${2 * sizeVariant}px ${2 * sizeVariant}px`,
|
borderRadius: '0 0 2px 2px',
|
||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
boxShadow: `inset 1px 0 2px rgba(255,255,255,0.3)`,
|
boxShadow: 'inset 1px 0 2px rgba(255,255,255,0.3)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -187,29 +184,28 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: `${8 * (1 - burnProgress)}px`,
|
top: `${8 * (1 - burnProgress)}px`,
|
||||||
right: `${10 + ((seed * 3) % 25)}%`,
|
right: `${10 + ((seed * 3) % 25)}%`,
|
||||||
width: `${3.5 * sizeVariant}px`,
|
width: '3.5px',
|
||||||
height: `${20 * heightVariant}px`,
|
height: `${20 * heightVariant}px`,
|
||||||
background: `linear-gradient(to bottom,
|
background: `linear-gradient(to bottom,
|
||||||
hsl(${34 + hueShift}, ${62 + hueShift}%, ${80 + brightnessShift * 10}%) 0%,
|
hsl(${34 + hueShift}, ${62 + hueShift}%, ${80 + brightnessShift * 10}%) 0%,
|
||||||
hsl(${34 + hueShift}, ${58 + hueShift}%, ${83 + brightnessShift * 10}%) 60%,
|
hsl(${34 + hueShift}, ${58 + hueShift}%, ${83 + brightnessShift * 10}%) 60%,
|
||||||
transparent 100%)`,
|
transparent 100%)`,
|
||||||
borderRadius: `0 0 ${2 * sizeVariant}px ${2 * sizeVariant}px`,
|
borderRadius: '0 0 2px 2px',
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
boxShadow: `inset -1px 0 2px rgba(255,255,255,0.2)`,
|
boxShadow: 'inset -1px 0 2px rgba(255,255,255,0.2)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Additional smaller drip for realism */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: `${4 * (1 - burnProgress)}px`,
|
top: `${4 * (1 - burnProgress)}px`,
|
||||||
left: `${45 + (seed % 20)}%`,
|
left: `${45 + (seed % 20)}%`,
|
||||||
width: `${2 * sizeVariant}px`,
|
width: '2px',
|
||||||
height: `${10 * heightVariant}px`,
|
height: `${10 * heightVariant}px`,
|
||||||
background: `linear-gradient(to bottom,
|
background: `linear-gradient(to bottom,
|
||||||
hsl(${35 + hueShift}, ${58 + hueShift}%, ${84 + brightnessShift * 10}%),
|
hsl(${35 + hueShift}, ${58 + hueShift}%, ${84 + brightnessShift * 10}%),
|
||||||
transparent)`,
|
transparent)`,
|
||||||
borderRadius: `0 0 ${1 * sizeVariant}px ${1 * sizeVariant}px`,
|
borderRadius: '0 0 1px 1px',
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -217,20 +213,19 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
|
|||||||
|
|
||||||
{/* Name Label */}
|
{/* Name Label */}
|
||||||
<p
|
<p
|
||||||
className="text-amber-200/50 font-cormorant italic mt-2 text-center leading-tight"
|
className="text-amber-200/60 font-cormorant italic mt-2 text-center leading-tight truncate"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
textShadow: '0 0 8px rgba(196,160,74,0.15)',
|
textShadow: '0 0 8px rgba(196,160,74,0.15)',
|
||||||
maxWidth: `${80 * sizeVariant}px`,
|
maxWidth: '68px',
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{candle.name}
|
{candle.name}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className="text-amber-200/35 font-lora text-center leading-tight"
|
className="text-amber-200/30 font-lora text-center leading-tight"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '8px',
|
fontSize: '7px',
|
||||||
marginTop: '2px',
|
marginTop: '2px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -512,7 +507,7 @@ export default function CandleSection() {
|
|||||||
|
|
||||||
{/* Candle Grid - with better spacing for many candles */}
|
{/* Candle Grid - with better spacing for many candles */}
|
||||||
{candles.length > 0 && (
|
{candles.length > 0 && (
|
||||||
<div className="flex flex-wrap items-end justify-center gap-2 sm:gap-3 mb-12 max-w-4xl mx-auto">
|
<div className="flex flex-wrap items-end justify-center gap-4 sm:gap-5 mb-12 max-w-4xl mx-auto">
|
||||||
{candles.map((candle, i) => (
|
{candles.map((candle, i) => (
|
||||||
<SingleCandle key={candle.id} candle={candle} index={i} />
|
<SingleCandle key={candle.id} candle={candle} index={i} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user