diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 1cfd3b3..e403d1b 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -18,6 +18,7 @@ import { } from 'lucide-react' import { useStore } from '../store/store' import ThemeToggle from './ThemeToggle' +import AssistWidget from './assist/AssistWidget' import logo from '../assets/hinterstein/logo.png' const navItems = [ @@ -147,6 +148,9 @@ export default function AppLayout() { + + {/* KI-Assistent als einblendbares Widget (nur Verwaltung) */} + ) } diff --git a/src/components/assist/AssistChat.tsx b/src/components/assist/AssistChat.tsx new file mode 100644 index 0000000..4c6a829 --- /dev/null +++ b/src/components/assist/AssistChat.tsx @@ -0,0 +1,154 @@ +import { useRef, useState } from 'react' +import { Bot, CircleAlert, Eraser, Send, UserRound } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { apiPost } from '../../lib/api' + +export interface Nachricht { + rolle: 'user' | 'assistant' + text: string +} + +const BEISPIELE = [ + 'Welche Wohnungen stehen leer?', + 'Wie hoch ist das monatliche Miet-Soll?', + 'Gibt es offene Schadensmeldungen?', + 'Bei welchen Zählern gab es Auffälligkeiten?', + 'Wie hoch waren die umlegbaren Kosten 2026?', +] + +/** Chat-Logik + UI des KI-Assistenten (v1.38 als Widget). + * Wird von der Assistenten-Seite und vom einblendbaren Widget genutzt. + * Der Verlauf lebt nur in dieser Sitzung (keine Speicherung). */ +export default function AssistChat() { + const [nachrichten, setNachrichten] = useState([]) + const [eingabe, setEingabe] = useState('') + const [sende, setSende] = useState(false) + const [fehler, setFehler] = useState(null) + const endeRef = useRef(null) + + const scrollEnde = () => setTimeout(() => endeRef.current?.scrollIntoView({ behavior: 'smooth' }), 50) + + const senden = async (frage: string) => { + const text = frage.trim() + if (!text || sende) return + setSende(true) + setFehler(null) + const verlauf = [...nachrichten, { rolle: 'user' as const, text }] + setNachrichten(verlauf) + setEingabe('') + try { + const { antwort } = await apiPost<{ antwort: string }>('/api/assist', { + frage: text, + verlauf: verlauf.slice(-20), + }) + setNachrichten((x) => [...x, { rolle: 'assistant', text: antwort }]) + } catch (e) { + setFehler(e instanceof Error ? e.message : 'Assistent antwortet nicht.') + // Nutzerfrage zurücknehmen, damit der Verlauf nicht kippt + setNachrichten((x) => x.slice(0, -1)) + } finally { + setSende(false) + scrollEnde() + } + } + + return ( +
+ {/* Nachrichtenbereich */} +
+ {nachrichten.length > 0 && ( + + )} + {nachrichten.length === 0 ? ( +
+ +

+ Stellen Sie eine Frage zu Ihrem Bestand – zum Beispiel: +

+
+ {BEISPIELE.map((b) => ( + + ))} +
+
+ ) : ( + nachrichten.map((m, i) => ( +
+ {m.rolle === 'assistant' && ( + + + + )} +
+ {m.text} +
+ {m.rolle === 'user' && ( + + + + )} +
+ )) + )} + {sende && ( +
+ Kimi denkt nach … +
+ )} + {fehler && ( +
+ {fehler} +
+ )} +
+
+ + {/* Eingabe */} +
{ + e.preventDefault() + senden(eingabe) + }} + > +