stadtwerke/innungsapp/apps/admin/components/ai-generator.tsx

182 lines
7.8 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { Sparkles, Copy, Check } from 'lucide-react'
import { trpc } from '@/lib/trpc-client'
interface AIGeneratorProps {
type: 'news' | 'stelle'
onApply?: (text: string) => void
}
const THINKING_STEPS = [
'KI denkt nach…',
'Thema wird analysiert…',
'Recherchiere Inhalte…',
'Struktur wird geplant…',
'Einleitung wird formuliert…',
'Hauptteil wird ausgearbeitet…',
'Formulierungen werden verfeinert…',
'Fachbegriffe werden geprüft…',
'Absätze werden aufgeteilt…',
'Zwischenüberschriften werden gesetzt…',
'Stil wird angepasst…',
'Rechtschreibung wird kontrolliert…',
'Markdown wird formatiert…',
'Überschrift wird optimiert…',
'Fazit wird formuliert…',
'Länge wird angepasst…',
'Ton wird auf Zielgruppe abgestimmt…',
'Aufzählungen werden erstellt…',
'Fettungen werden gesetzt…',
'Satzfluss wird geprüft…',
'Grammatik wird überprüft…',
'Keywords werden eingebaut…',
'Einleitung wird überarbeitet…',
'Abschnitte werden umstrukturiert…',
'Wiederholungen werden entfernt…',
'Zeichensetzung wird geprüft…',
'Leerzeilen werden optimiert…',
'Fachlich wird validiert…',
'Lesbarkeit wird verbessert…',
'Zusammenfassung wird erstellt…',
'Text wird poliert…',
'Letzte Korrekturen…',
'Fast fertig…',
]
export function AIGenerator({ type, onApply }: AIGeneratorProps) {
const { data: org } = trpc.organizations.me.useQuery()
const [prompt, setPrompt] = useState('')
const [format, setFormat] = useState('markdown')
const [loading, setLoading] = useState(false)
const [generatedText, setGeneratedText] = useState('')
const [copied, setCopied] = useState(false)
const [stepIndex, setStepIndex] = useState(0)
useEffect(() => {
if (!loading) { setStepIndex(0); return }
const interval = setInterval(() => {
setStepIndex((i) => (i + 1) % THINKING_STEPS.length)
}, 5000)
return () => clearInterval(interval)
}, [loading])
async function handleGenerate() {
if (!prompt.trim()) return
setLoading(true)
setGeneratedText('')
try {
const res = await fetch('/api/ai/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, type, format }),
})
if (!res.ok) {
throw new Error('Fehler bei der Generierung')
}
const data = await res.json()
setGeneratedText(data.text)
} catch (err) {
alert((err as Error).message)
} finally {
setLoading(false)
}
}
function handleCopy() {
navigator.clipboard.writeText(generatedText)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
if (org && !org.aiEnabled) return null
return (
<div className="bg-white rounded-xl border border-brand-100 shadow-sm p-6 space-y-4 flex flex-col h-full bg-gradient-to-br from-white to-brand-50/20">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-bold text-gray-900">KI-Assistent</h2>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{type === 'news' ? 'Worum geht es in dem News-Beitrag?' : 'Beschreiben Sie die Stelle für die Lehrlingsbörse'}
</label>
<textarea
rows={3}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder={type === 'news' ? "Schreibe einen Artikel über..." : "Eine kurze Zusammenfassung der Aufgaben..."}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
</div>
<div className="flex items-center justify-between">
<select
value={format}
onChange={(e) => setFormat(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 bg-white"
>
<option value="markdown">Markdown Format</option>
<option value="text">Einfacher Text</option>
</select>
<button
type="button"
onClick={handleGenerate}
disabled={loading || !prompt.trim()}
className="flex items-center gap-2 bg-brand-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-brand-600 disabled:opacity-60 transition-colors"
>
{loading ? 'Generiere...' : 'Generieren'}
<Sparkles className="w-4 h-4" />
</button>
</div>
{loading && (
<div className="flex items-center gap-3 px-4 py-3 bg-brand-50 border border-brand-100 rounded-lg">
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-brand-400 animate-bounce [animation-delay:0ms]" />
<span className="w-2 h-2 rounded-full bg-brand-400 animate-bounce [animation-delay:150ms]" />
<span className="w-2 h-2 rounded-full bg-brand-400 animate-bounce [animation-delay:300ms]" />
</div>
<span className="text-sm text-brand-700 font-medium transition-all">{THINKING_STEPS[stepIndex]}</span>
</div>
)}
{generatedText && (
<div className="mt-4 flex-1 flex flex-col min-h-[300px] space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Ergebnis:</span>
<div className="flex gap-4">
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1 text-xs text-brand-600 hover:text-brand-700 font-medium transition-colors"
>
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
{copied ? 'Kopiert!' : 'Kopieren'}
</button>
{onApply && (
<button
type="button"
onClick={() => onApply(generatedText)}
className="flex items-center gap-1 text-xs text-brand-600 hover:text-brand-700 font-medium transition-colors"
>
<Check className="w-4 h-4" />
Übernehmen
</button>
)}
</div>
</div>
<textarea
readOnly
value={generatedText}
className="w-full flex-1 p-3 border border-gray-200 rounded-lg text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-500/50"
/>
</div>
)}
</div>
)
}