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...`)
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
VERBOTEN: Beleidigungen, Spam, Hassrede, Werbung, völlig zusammenhanglose oder sinnlose Texte ohne Bezug
WICHTIG: Sei SEHR großzügig! Die meisten Beiträge sind von Familienmitgliedern und Freunden.
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:
{"appropriate": true} oder {"appropriate": false, "reason": "kurze Begründung"}
JSON:`
const controller = new AbortController()
setTimeout(() => controller.abort(), 15000)
const timeout = setTimeout(() => controller.abort(), 15000)
const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434'
const res = await fetch(`${ollamaUrl}/api/generate`, {
@@ -83,11 +96,13 @@ JSON:`
})
if (!res.ok) {
clearTimeout(timeout)
console.warn(`[AI-Mod] Ollama error: ${res.status}`)
return
}
const data = await res.json()
clearTimeout(timeout)
const responseText = (data.response || '').trim()
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' },
body: JSON.stringify({
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'}
Titel: ${title || 'Kein Titel'}
Text: "${text}"
Unangemessen sind:
- Spam, Werbung, Links zu Produkten
- Beleidigungen, Hassrede
- Völlig irrelevanter Inhalt
- Unseriöse oder respektlose Inhalte
ERLAUBT (immer appropriate=true):
- Kurze Beschreibungen, Namen, Orte, Daten - das sind Erinnerungen!
- Persönliche Geschichten, Beileidsbekundungen, Liebe, Vermissen, Trauer
- Auch sehr kurze oder allgemeine Texte sind OK
Angemessen sind:
- Erinnerungen, Anekdoten
- Kondolenzen, Beileidsbekundungen
- Persönliche Geschichten
- Emotionale oder traurige Texte
NUR VERBOTEN (appropriate=false):
- Beleidigungen, Hassrede, Schimpfwörter
- Offensichtlicher Spam oder Werbung mit Links
- Komplett sinnloser Text (Tastatur-Spam)
Im Zweifel: appropriate=true
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 }) {
// Generate consistent but varied properties based on candle ID
const seed = candle.id * 7919 // Prime number for good distribution
const sizeVariant = ((seed % 5) / 4) * 0.6 + 0.7 // 0.7 to 1.3
const heightVariant = ((seed % 7) / 6) * 0.5 + 0.75 // 0.75 to 1.25
const hueShift = (seed % 20) - 10 // -10 to +10 hue shift
const brightnessShift = ((seed % 15) - 7) / 100 // -0.07 to +0.08 brightness
const rotation = ((seed % 11) - 5) / 2 // -2.5 to +2.5 degrees
const seed = candle.id * 7919
const heightVariant = ((seed % 7) / 6) * 0.3 + 0.85 // 0.85 to 1.15 (subtler)
const hueShift = (seed % 14) - 7 // -7 to +7
const brightnessShift = ((seed % 11) - 5) / 100
const rotation = ((seed % 7) - 3) / 3 // -1 to +1 degrees (subtler)
// Calculate burn-down based on age
const createdTime = new Date(candle.created_at + 'Z').getTime()
const now = Date.now()
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 candleWidth = 36 * sizeVariant
const flameSize = 1.0 * sizeVariant
const candleHeight = 72 * heightVariant * (1 - burnProgress)
const candleWidth = 32
const flameSize = 0.95
const delay = (index % 7) * 0.15
return (
@@ -114,12 +111,12 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.6, delay: Math.min(index * 0.08, 1.2) }}
style={{
display: 'inline-flex',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
transform: `rotate(${rotation}deg)`,
filter: `brightness(${1 + brightnessShift})`,
marginLeft: index > 0 ? '-8px' : '0', // Slight overlap for natural clustering
width: 72,
}}
className="group relative"
>
@@ -141,7 +138,7 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
<div
style={{
width: 2,
height: 6 * sizeVariant,
height: 6,
background: 'linear-gradient(to bottom, #2b1a0a, #1a0f06)',
borderRadius: 1,
}}
@@ -155,7 +152,7 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
background: `linear-gradient(to bottom,
hsl(${35 + hueShift}, ${65 + hueShift}%, ${88 + brightnessShift * 10}%) 0%,
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: `
inset 2px 0 4px rgba(255,255,255,${0.4 + 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',
}}
>
{/* Wax drips - more visible */}
{/* Wax drips */}
<div
style={{
position: 'absolute',
top: 0,
left: `${15 + (seed % 30)}%`,
width: `${4 * sizeVariant}px`,
width: '4px',
height: `${16 * heightVariant}px`,
background: `linear-gradient(to bottom,
hsl(${33 + hueShift}, ${60 + hueShift}%, ${82 + brightnessShift * 10}%) 0%,
hsl(${33 + hueShift}, ${55 + hueShift}%, ${85 + brightnessShift * 10}%) 50%,
transparent 100%)`,
borderRadius: `0 0 ${2 * sizeVariant}px ${2 * sizeVariant}px`,
borderRadius: '0 0 2px 2px',
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
@@ -187,29 +184,28 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
position: 'absolute',
top: `${8 * (1 - burnProgress)}px`,
right: `${10 + ((seed * 3) % 25)}%`,
width: `${3.5 * sizeVariant}px`,
width: '3.5px',
height: `${20 * heightVariant}px`,
background: `linear-gradient(to bottom,
hsl(${34 + hueShift}, ${62 + hueShift}%, ${80 + brightnessShift * 10}%) 0%,
hsl(${34 + hueShift}, ${58 + hueShift}%, ${83 + brightnessShift * 10}%) 60%,
transparent 100%)`,
borderRadius: `0 0 ${2 * sizeVariant}px ${2 * sizeVariant}px`,
borderRadius: '0 0 2px 2px',
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
style={{
position: 'absolute',
top: `${4 * (1 - burnProgress)}px`,
left: `${45 + (seed % 20)}%`,
width: `${2 * sizeVariant}px`,
width: '2px',
height: `${10 * heightVariant}px`,
background: `linear-gradient(to bottom,
hsl(${35 + hueShift}, ${58 + hueShift}%, ${84 + brightnessShift * 10}%),
transparent)`,
borderRadius: `0 0 ${1 * sizeVariant}px ${1 * sizeVariant}px`,
borderRadius: '0 0 1px 1px',
opacity: 0.6,
}}
/>
@@ -217,20 +213,19 @@ function SingleCandle({ candle, index }: { candle: CandleData; index: number })
{/* Name Label */}
<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={{
fontSize: '11px',
textShadow: '0 0 8px rgba(196,160,74,0.15)',
maxWidth: `${80 * sizeVariant}px`,
wordBreak: 'break-word',
maxWidth: '68px',
}}
>
{candle.name}
</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={{
fontSize: '8px',
fontSize: '7px',
marginTop: '2px',
}}
>
@@ -512,7 +507,7 @@ export default function CandleSection() {
{/* Candle Grid - with better spacing for many candles */}
{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) => (
<SingleCandle key={candle.id} candle={candle} index={i} />
))}