push
This commit is contained in:
181
innungsapp/apps/admin/components/ai-generator.tsx
Normal file
181
innungsapp/apps/admin/components/ai-generator.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user