Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
import React, { useState, useRef, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import aiService from '../../services/aiService';
import { marked } from 'marked';
marked.use({ breaks: true, gfm: true });
const renderMd = (text) => {
try {
const html = marked.parse(String(text || ''), { async: false });
return { __html: typeof html === 'string' ? html : String(html) };
} catch {
return { __html: String(text || '').replace(/\n/g, '<br>') };
}
};
const STAFF_ROLES = ['super_admin', 'admin', 'support', 'bearbeiter'];
const WELCOME_MSG = {
role: 'assistant',
content: 'Hallo! Ich bin dein KI-Assistent für IT-Support. Wie kann ich dir helfen?\n\nDu kannst mich z.B. nach Troubleshooting-Schritten, Lösungen für bekannte Probleme oder Ticket-Einschätzungen fragen.',
};
export default function AiChatWidget() {
const { user } = useAuth();
const location = useLocation();
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState([WELCOME_MSG]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [configured, setConfigured] = useState(true);
const bottomRef = useRef(null);
const inputRef = useRef(null);
const isStaff = user && STAFF_ROLES.includes(user.role_name);
const isPublicPage = ['/health', '/defect', '/login', '/tv'].some(p => location.pathname.startsWith(p));
// Status prüfen (immer, auch wenn nicht Staff)
useEffect(() => {
if (!isStaff) return;
aiService.getStatus()
.then(s => setConfigured(s.configured))
.catch(() => setConfigured(false));
}, [isStaff]);
useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [open]);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Nur für Staff-Rollen und nicht auf öffentlichen Seiten anzeigen
if (!isStaff || isPublicPage) return null;
async function sendMessage() {
const text = input.trim();
if (!text || loading) return;
const userMsg = { role: 'user', content: text };
const newMessages = [...messages, userMsg];
setMessages(newMessages);
setInput('');
setLoading(true);
try {
// Nur die letzten 20 Nachrichten als Kontext senden (ohne Welcome-Msg)
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 || 'Unbekannter Fehler'}`,
isError: true,
}]);
} finally {
setLoading(false);
setTimeout(() => inputRef.current?.focus(), 50);
}
}
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function clearChat() {
setMessages([WELCOME_MSG]);
}
return (
<>
{/* Floating Button */}
<button
onClick={() => setOpen(o => !o)}
style={{
position: 'fixed',
bottom: 24,
right: 24,
width: 52,
height: 52,
borderRadius: '50%',
background: configured ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : '#6b7280',
border: 'none',
color: '#fff',
fontSize: 22,
cursor: 'pointer',
boxShadow: '0 4px 16px rgba(99,102,241,0.4)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'transform 0.2s',
}}
title={configured ? 'KI-Assistent' : 'KI nicht konfiguriert'}
>
{open ? '✕' : '🤖'}
</button>
{/* Chat Panel */}
{open && (
<div style={{
position: 'fixed',
bottom: 88,
right: 24,
width: 380,
maxWidth: 'calc(100vw - 48px)',
height: 520,
background: 'var(--bg-primary, #fff)',
border: '1px solid var(--border-color, #e5e7eb)',
borderRadius: 16,
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
display: 'flex',
flexDirection: 'column',
zIndex: 999,
overflow: 'hidden',
}}>
{/* Header */}
<div style={{
padding: '12px 16px',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
color: '#fff',
display: 'flex',
alignItems: 'center',
gap: 10,
}}>
<span style={{ fontSize: 20 }}>🤖</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 14 }}>KI-Assistent</div>
<div style={{ fontSize: 11, opacity: 0.8 }}>
{configured ? 'Claude claude-sonnet-4-6 · IT Support' : 'Nicht konfiguriert'}
</div>
</div>
<button
onClick={clearChat}
title="Chat leeren"
style={{
background: 'rgba(255,255,255,0.2)',
border: 'none',
color: '#fff',
borderRadius: 6,
padding: '2px 8px',
fontSize: 11,
cursor: 'pointer',
}}
>
Leeren
</button>
</div>
{!configured && (
<div style={{
padding: '10px 16px',
background: '#fef3c7',
color: '#92400e',
fontSize: 12,
borderBottom: '1px solid #fde68a',
}}>
ANTHROPIC_API_KEY nicht konfiguriert
</div>
)}
{/* Messages */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '12px 14px',
display: 'flex',
flexDirection: 'column',
gap: 10,
}}>
{messages.map((msg, i) => (
<div key={i} style={{
display: 'flex',
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
}}>
<div style={{
maxWidth: '85%',
padding: '8px 12px',
borderRadius: msg.role === 'user' ? '12px 12px 2px 12px' : '12px 12px 12px 2px',
background: msg.role === 'user'
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: msg.isError
? '#fee2e2'
: 'var(--bg-secondary, #f3f4f6)',
color: msg.role === 'user' ? '#fff' : msg.isError ? '#991b1b' : 'var(--text-primary, #111)',
fontSize: 13,
lineHeight: 1.5,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}>
{msg.role === 'assistant'
? <div className="md-content" dangerouslySetInnerHTML={renderMd(msg.content)} />
: msg.content}
</div>
</div>
))}
{loading && (
<div style={{ display: 'flex', justifyContent: 'flex-start' }}>
<div style={{
padding: '10px 14px',
borderRadius: '12px 12px 12px 2px',
background: 'var(--bg-secondary, #f3f4f6)',
fontSize: 18,
letterSpacing: 2,
}}>
<span style={{ animation: 'pulse 1.2s infinite' }}></span>
<span style={{ animation: 'pulse 1.2s 0.3s infinite', opacity: 0.5 }}></span>
<span style={{ animation: 'pulse 1.2s 0.6s infinite', opacity: 0.25 }}></span>
</div>
</div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<div style={{
padding: '10px 12px',
borderTop: '1px solid var(--border-color, #e5e7eb)',
display: 'flex',
gap: 8,
}}>
<textarea
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Nachricht eingeben... (Enter zum Senden)"
disabled={loading || !configured}
rows={2}
style={{
flex: 1,
resize: 'none',
padding: '8px 10px',
borderRadius: 8,
border: '1px solid var(--border-color, #d1d5db)',
fontSize: 13,
background: 'var(--bg-primary, #fff)',
color: 'var(--text-primary, #111)',
outline: 'none',
fontFamily: 'inherit',
}}
/>
<button
onClick={sendMessage}
disabled={loading || !input.trim() || !configured}
style={{
padding: '0 14px',
borderRadius: 8,
border: 'none',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
color: '#fff',
fontSize: 18,
cursor: 'pointer',
opacity: loading || !input.trim() || !configured ? 0.5 : 1,
alignSelf: 'flex-end',
height: 38,
}}
>
</button>
</div>
</div>
)}
</>
);
}