297 lines
12 KiB
JavaScript
297 lines
12 KiB
JavaScript
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>
|
||
)}
|
||
</>
|
||
);
|
||
}
|