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:
denshooter
2026-02-21 20:37:14 +01:00
parent c184f5c679
commit 6e2ef55cee
3 changed files with 58 additions and 48 deletions
+20 -5
View File
@@ -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}"`)
+11 -11
View File
@@ -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:
{ {
+27 -32
View File
@@ -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} />
))} ))}