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...`)
|
||||
|
||||
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}"`)
|
||||
|
||||
|
||||
@@ -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:
|
||||
{
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user