2026-06-01 20:49:07 +02:00
import React , { useState , useEffect , useRef } from 'react' ;
import { useAuth } from '../context/AuthContext' ;
import aiService from '../services/aiService' ;
import { toast } from 'react-toastify' ;
2026-06-26 13:27:17 +02:00
import { renderMd } from '../utils/sanitizeMarkdown' ;
2026-06-01 20:49:07 +02:00
const WELCOME _MSG = {
role : 'assistant' ,
content : 'Hallo! Ich bin dein KI-Assistent für IT-Support. Du kannst mich nach Lösungen für bekannte Probleme, Troubleshooting-Schritten oder Ticket-Einschätzungen fragen.' ,
} ;
const CATEGORY _LIST = [ 'Software' , 'Hardware' , 'Allgemein' , 'SelectLine' ] ;
export default function AiPage ( ) {
const { isAdmin } = useAuth ( ) ;
const [ activeTab , setActiveTab ] = useState ( 'chat' ) ;
// Chat State
const [ messages , setMessages ] = useState ( [ WELCOME _MSG ] ) ;
const [ input , setInput ] = useState ( '' ) ;
const [ chatLoading , setChatLoading ] = useState ( false ) ;
const [ configured , setConfigured ] = useState ( true ) ;
const bottomRef = useRef ( null ) ;
const inputRef = useRef ( null ) ;
// Knowledge Base State
const [ kbEntries , setKbEntries ] = useState ( [ ] ) ;
const [ kbLoading , setKbLoading ] = useState ( false ) ;
const [ showKbForm , setShowKbForm ] = useState ( false ) ;
const [ kbForm , setKbForm ] = useState ( { problem : '' , solution : '' , category : 'Allgemein' , tags : '' } ) ;
const [ kbSaving , setKbSaving ] = useState ( false ) ;
useEffect ( ( ) => {
aiService . getStatus ( )
. then ( s => setConfigured ( s . configured ) )
. catch ( ( ) => setConfigured ( false ) ) ;
} , [ ] ) ;
useEffect ( ( ) => {
if ( activeTab === 'kb' ) loadKb ( ) ;
} , [ activeTab ] ) ;
useEffect ( ( ) => {
bottomRef . current ? . scrollIntoView ( { behavior : 'smooth' } ) ;
} , [ messages ] ) ;
async function loadKb ( ) {
setKbLoading ( true ) ;
try {
const data = await aiService . getKnowledgeBase ( ) ;
setKbEntries ( data ) ;
} catch ( err ) {
toast . error ( 'Wissensdatenbank konnte nicht geladen werden' ) ;
} finally {
setKbLoading ( false ) ;
}
}
async function sendMessage ( ) {
const text = input . trim ( ) ;
if ( ! text || chatLoading ) return ;
const userMsg = { role : 'user' , content : text } ;
const newMessages = [ ... messages , userMsg ] ;
setMessages ( newMessages ) ;
setInput ( '' ) ;
setChatLoading ( true ) ;
try {
const contextMsgs = newMessages . filter ( m => m !== WELCOME _MSG ) . slice ( - 20 ) ;
const reply = await aiService . chat ( contextMsgs ) ;
setMessages ( prev => [ ... prev , { role : 'assistant' , content : reply } ] ) ;
} catch ( err ) {
setMessages ( prev => [ ... prev , {
role : 'assistant' ,
content : ` Fehler: ${ err . response ? . data ? . message || err . message } ` ,
isError : true ,
} ] ) ;
} finally {
setChatLoading ( false ) ;
setTimeout ( ( ) => inputRef . current ? . focus ( ) , 50 ) ;
}
}
async function saveKbEntry ( ) {
if ( ! kbForm . problem . trim ( ) || ! kbForm . solution . trim ( ) ) {
toast . error ( 'Problem und Lösung sind pflichtfelder' ) ;
return ;
}
setKbSaving ( true ) ;
try {
await aiService . addKnowledgeEntry ( kbForm ) ;
toast . success ( 'Eintrag gespeichert' ) ;
setKbForm ( { problem : '' , solution : '' , category : 'Allgemein' , tags : '' } ) ;
setShowKbForm ( false ) ;
loadKb ( ) ;
} catch ( err ) {
toast . error ( 'Fehler beim Speichern' ) ;
} finally {
setKbSaving ( false ) ;
}
}
async function deleteKbEntry ( id ) {
if ( ! window . confirm ( 'Eintrag wirklich löschen?' ) ) return ;
try {
await aiService . deleteKnowledgeEntry ( id ) ;
toast . success ( 'Eintrag gelöscht' ) ;
setKbEntries ( prev => prev . filter ( e => e . id !== id ) ) ;
} catch {
toast . error ( 'Fehler beim Löschen' ) ;
}
}
const TAB = ( active ) => ( {
padding : '10px 16px' ,
border : 'none' ,
background : 'transparent' ,
cursor : 'pointer' ,
fontSize : '0.875rem' ,
fontWeight : active ? 600 : 400 ,
color : active ? 'var(--cereda-primary)' : 'var(--text-muted)' ,
borderBottom : active ? '2px solid var(--cereda-primary)' : '2px solid transparent' ,
} ) ;
return (
< div className = "main-content" >
< div style = { { maxWidth : 960 , margin : '0 auto' } } >
{ /* Header */ }
< div style = { { marginBottom : 24 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 , marginBottom : 4 } } >
< span style = { { fontSize : 28 } } > 🤖 < / span >
< h1 style = { { margin : 0 , fontSize : '1.5rem' , fontWeight : 700 } } > KI - Assistent < / h1 >
{ ! configured && (
< span style = { { fontSize : 12 , padding : '2px 8px' , borderRadius : 6 , background : '#fef3c7' , color : '#92400e' , border : '1px solid #fde68a' } } >
Nicht konfiguriert
< / span >
) }
< / div >
< p style = { { margin : 0 , color : 'var(--text-muted)' , fontSize : '0.875rem' } } >
Claude claude - sonnet - 4 - 6 · IT - Support - Assistent mit Wissensdatenbank
< / p >
< / div >
{ ! configured && (
< div className = "card" style = { { padding : '16px 20px' , marginBottom : 20 , background : '#fef3c7' , border : '1px solid #fde68a' } } >
< strong > ⚠ ️ ANTHROPIC _API _KEY fehlt < / strong >
< p style = { { margin : '4px 0 0' , fontSize : '0.875rem' , color : '#92400e' } } >
Füge < code > ANTHROPIC _API _KEY = sk - ant - ... < / code > in der docker - compose . yml als Umgebungsvariable hinzu und deploye neu .
< / p >
< / div >
) }
< div className = "card" style = { { overflow : 'hidden' } } >
{ /* Tabs */ }
< div style = { { display : 'flex' , borderBottom : '1px solid var(--border-color)' , padding : '0 8px' } } >
< button style = { TAB ( activeTab === 'chat' ) } onClick = { ( ) => setActiveTab ( 'chat' ) } >
💬 Chat
< / button >
< button style = { TAB ( activeTab === 'kb' ) } onClick = { ( ) => setActiveTab ( 'kb' ) } >
📚 Wissensdatenbank ( { kbEntries . length } )
< / button >
< / div >
{ /* Chat Tab */ }
{ activeTab === 'chat' && (
< div style = { { display : 'flex' , flexDirection : 'column' , height : 560 } } >
{ /* Messages */ }
< div style = { { flex : 1 , overflowY : 'auto' , padding : '20px 24px' , display : 'flex' , flexDirection : 'column' , gap : 12 } } >
{ messages . map ( ( msg , i ) => (
< div key = { i } style = { { display : 'flex' , justifyContent : msg . role === 'user' ? 'flex-end' : 'flex-start' } } >
< div style = { {
maxWidth : '75%' ,
padding : '10px 14px' ,
borderRadius : msg . role === 'user' ? '14px 14px 2px 14px' : '14px 14px 14px 2px' ,
background : msg . role === 'user'
? 'linear-gradient(135deg, var(--cereda-primary), #8b5cf6)'
: msg . isError ? '#fee2e2' : 'var(--bg-tertiary)' ,
color : msg . role === 'user' ? '#fff' : msg . isError ? '#991b1b' : 'var(--text-primary)' ,
fontSize : '0.875rem' ,
lineHeight : 1.6 ,
whiteSpace : 'pre-wrap' ,
wordBreak : 'break-word' ,
boxShadow : '0 1px 3px rgba(0,0,0,0.06)' ,
} } >
{ msg . role === 'assistant'
? < div className = "md-content" dangerouslySetInnerHTML = { renderMd ( msg . content ) } / >
: msg . content }
< / div >
< / div >
) ) }
{ chatLoading && (
< div style = { { display : 'flex' } } >
< div style = { { padding : '10px 16px' , borderRadius : '14px 14px 14px 2px' , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' , fontSize : '0.875rem' } } >
KI denkt nach …
< / div >
< / div >
) }
< div ref = { bottomRef } / >
< / div >
{ /* Input */ }
< div style = { { padding : '12px 16px' , borderTop : '1px solid var(--border-color)' , display : 'flex' , gap : 10 , alignItems : 'flex-end' } } >
< textarea
ref = { inputRef }
value = { input }
onChange = { e => setInput ( e . target . value ) }
onKeyDown = { e => { if ( e . key === 'Enter' && ! e . shiftKey ) { e . preventDefault ( ) ; sendMessage ( ) ; } } }
placeholder = "Nachricht eingeben… (Enter zum Senden, Shift+Enter für Zeilenumbruch)"
disabled = { chatLoading || ! configured }
rows = { 2 }
style = { {
flex : 1 , resize : 'none' , padding : '10px 12px' , borderRadius : 8 ,
border : '1px solid var(--border-color)' , fontSize : '0.875rem' ,
background : 'var(--bg-primary)' , color : 'var(--text-primary)' ,
outline : 'none' , fontFamily : 'inherit' ,
} }
/ >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 , alignItems : 'stretch' } } >
< button
onClick = { sendMessage }
disabled = { chatLoading || ! input . trim ( ) || ! configured }
className = "btn btn-primary"
style = { { height : 38 , padding : '0 20px' , whiteSpace : 'nowrap' } }
>
Senden ↑
< / button >
< button
onClick = { ( ) => setMessages ( [ WELCOME _MSG ] ) }
className = "btn btn-secondary"
style = { { height : 38 , padding : '0 20px' , fontSize : '0.75rem' , whiteSpace : 'nowrap' } }
>
Leeren
< / button >
< / div >
< / div >
< / div >
) }
{ /* Knowledge Base Tab */ }
{ activeTab === 'kb' && (
< div style = { { padding : '20px 24px' } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 20 } } >
< div >
< h3 style = { { margin : 0 , fontSize : '1rem' } } > Bekannte Probleme & Lösungen < / h3 >
< p style = { { margin : '2px 0 0' , fontSize : '0.8125rem' , color : 'var(--text-muted)' } } >
Diese Einträge werden als Kontext an den KI - Assistenten weitergegeben .
< / p >
< / div >
{ isAdmin ( ) && (
< button className = "btn btn-primary" onClick = { ( ) => setShowKbForm ( f => ! f ) } >
+ Eintrag hinzufügen
< / button >
) }
< / div >
{ /* Add Form */ }
{ showKbForm && isAdmin ( ) && (
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 10 , padding : 20 , marginBottom : 20 } } >
< h4 style = { { margin : '0 0 16px' , fontSize : '0.9375rem' } } > Neuer Eintrag < / h4 >
< div style = { { display : 'grid' , gap : 12 } } >
< div >
< label style = { { fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'uppercase' , letterSpacing : '0.05em' , color : 'var(--text-muted)' , display : 'block' , marginBottom : 4 } } > Problem * < / label >
< textarea
value = { kbForm . problem }
onChange = { e => setKbForm ( f => ( { ... f , problem : e . target . value } ) ) }
placeholder = "Beschreibe das Problem"
rows = { 2 }
style = { { width : '100%' , padding : '8px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , fontSize : '0.875rem' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' , resize : 'vertical' , boxSizing : 'border-box' } }
/ >
< / div >
< div >
< label style = { { fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'uppercase' , letterSpacing : '0.05em' , color : 'var(--text-muted)' , display : 'block' , marginBottom : 4 } } > Lösung * < / label >
< textarea
value = { kbForm . solution }
onChange = { e => setKbForm ( f => ( { ... f , solution : e . target . value } ) ) }
placeholder = "Beschreibe die Lösung Schritt für Schritt"
rows = { 4 }
style = { { width : '100%' , padding : '8px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , fontSize : '0.875rem' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' , resize : 'vertical' , boxSizing : 'border-box' } }
/ >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div >
< label style = { { fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'uppercase' , letterSpacing : '0.05em' , color : 'var(--text-muted)' , display : 'block' , marginBottom : 4 } } > Kategorie < / label >
< select
value = { kbForm . category }
onChange = { e => setKbForm ( f => ( { ... f , category : e . target . value } ) ) }
style = { { width : '100%' , padding : '8px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , fontSize : '0.875rem' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' } }
>
{ CATEGORY _LIST . map ( c => < option key = { c } > { c } < / option > ) }
< / select >
< / div >
< div >
< label style = { { fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'uppercase' , letterSpacing : '0.05em' , color : 'var(--text-muted)' , display : 'block' , marginBottom : 4 } } > Tags < / label >
< input
type = "text"
value = { kbForm . tags }
onChange = { e => setKbForm ( f => ( { ... f , tags : e . target . value } ) ) }
placeholder = "z.B. VPN, Outlook, Drucker"
style = { { width : '100%' , padding : '8px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , fontSize : '0.875rem' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' , boxSizing : 'border-box' } }
/ >
< / div >
< / div >
< div style = { { display : 'flex' , gap : 8 , justifyContent : 'flex-end' } } >
< button className = "btn btn-secondary" onClick = { ( ) => setShowKbForm ( false ) } > Abbrechen < / button >
< button className = "btn btn-primary" onClick = { saveKbEntry } disabled = { kbSaving } >
{ kbSaving ? 'Speichern…' : 'Speichern' }
< / button >
< / div >
< / div >
< / div >
) }
{ /* Entries */ }
{ kbLoading ? (
< div style = { { textAlign : 'center' , padding : '3rem 0' , color : 'var(--text-muted)' } } > Lade … < / div >
) : kbEntries . length === 0 ? (
< div style = { { textAlign : 'center' , padding : '3rem 0' , color : 'var(--text-muted)' } } >
< div style = { { fontSize : '2.5rem' , marginBottom : 8 } } > 📚 < / div >
< p style = { { margin : 0 } } > Noch keine Einträge vorhanden . < / p >
{ isAdmin ( ) && < p style = { { margin : '4px 0 0' , fontSize : '0.8125rem' } } > Füge bekannte Probleme & Lösungen hinzu , damit die KI besser helfen kann . < / p > }
< / div >
) : (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 12 } } >
{ kbEntries . map ( entry => (
< div key = { entry . id } style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 10 , padding : '14px 18px' } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , gap : 12 } } >
< div style = { { flex : 1 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginBottom : 6 } } >
< span style = { { fontSize : '0.6875rem' , fontWeight : 700 , padding : '2px 8px' , borderRadius : 4 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' } } >
{ entry . category }
< / span >
{ entry . tags && (
< span style = { { fontSize : '0.6875rem' , color : 'var(--text-muted)' } } >
🏷 ️ { entry . tags }
< / span >
) }
< / div >
< div style = { { marginBottom : 8 } } >
< span style = { { fontSize : '0.6875rem' , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : '0.05em' , color : 'var(--text-muted)' } } > Problem : < / span >
< span style = { { fontSize : '0.875rem' , color : 'var(--text-primary)' , whiteSpace : 'pre-wrap' } } > { entry . problem } < / span >
< / div >
< div >
< span style = { { fontSize : '0.6875rem' , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : '0.05em' , color : 'var(--success)' } } > Lösung : < / span >
< span style = { { fontSize : '0.875rem' , color : 'var(--text-secondary)' , whiteSpace : 'pre-wrap' } } > { entry . solution } < / span >
< / div >
< / div >
{ isAdmin ( ) && (
< button
onClick = { ( ) => deleteKbEntry ( entry . id ) }
style = { { padding : '4px 8px' , borderRadius : 6 , border : '1px solid var(--border-color)' , background : 'transparent' , color : 'var(--danger)' , cursor : 'pointer' , fontSize : '0.8125rem' , flexShrink : 0 } }
title = "Löschen"
>
🗑 ️
< / button >
) }
< / div >
< div style = { { marginTop : 8 , fontSize : '0.75rem' , color : 'var(--text-muted)' } } >
Erstellt von { entry . created _by _username || 'System' } · { new Date ( entry . created _at ) . toLocaleDateString ( 'de-DE' ) }
< / div >
< / div >
) ) }
< / div >
) }
< / div >
) }
< / div >
< / div >
< / div >
) ;
}