182 lines
7.6 KiB
TypeScript
182 lines
7.6 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>
|
|
)
|
|
}
|